# Platform Notes

Where each platform keeps content, users, URLs, files and schedules, and the traps specific to it. Table and column names below are the platform defaults; confirm them against the actual install (prefixes, versions and plugins change them) before writing export queries.

## Contents

- [WordPress](#wordpress)
- [Drupal 8 and later](#drupal-8-and-later)
- [Joomla 4 and 5](#joomla-4-and-5)
- [Magento 2](#magento-2)
- [Laravel](#laravel)
- [Symfony](#symfony)
- [CodeIgniter 4](#codeigniter-4)
- [Plain PHP](#plain-php)

## WordPress

- **Prefix:** `$table_prefix` in `wp-config.php` (default `wp_`). Use the real prefix in every query.
- **Content:** `wp_posts`. Filter `post_type IN ('post', 'page')` (plus any custom types the site uses) and `post_status = 'publish'`; `post_name` is the slug, `post_date_gmt` / `post_modified_gmt` are UTC. Rows with `post_type = 'revision'`, `'attachment'` or `'nav_menu_item'` are not pages.
- **Rendered HTML:** `post_content` holds block comments (`<!-- wp:paragraph -->`) and shortcodes (`[gallery ids="1,2"]`) that only WordPress can expand. Export the rendered HTML with WordPress itself while the old site still runs, for example with WP-CLI: `wp eval 'echo apply_filters("the_content", get_post(123)->post_content);'`, looping over `wp post list --post_type=page,post --post_status=publish --field=ID`.
- **Metadata and options:** `wp_postmeta` and `wp_options` often hold serialized PHP (`a:2:{s:3:"foo";...}`). Decode it to JSON during export with the PHP CLI, refusing to instantiate objects: `php -r 'echo json_encode(unserialize(stream_get_contents(STDIN), ["allowed_classes" => false]));'`.
- **Taxonomies:** `wp_terms`, `wp_term_taxonomy`, `wp_term_relationships`.
- **Users:** `wp_users` (`user_email`, `display_name`; never `user_pass`) and roles in `wp_usermeta` under `<prefix>capabilities` (serialized).
- **URLs:** the `permalink_structure` option (for example `/%year%/%monthnum%/%postname%/`). With plain permalinks, URLs are `/?p=123` and `/?page_id=5`; they land on `/`, so the mapping script must run in the home page (`old-urls.md`).
- **Files:** `wp-content/uploads/YYYY/MM/`, including generated thumbnail sizes. Copy the ones still referenced into the static-site `dir` at the same paths so old image URLs keep working.
- **Schedules:** WP-Cron (`wp cron event list`), triggered by page visits rather than the system cron.
- **Plugins:** every active plugin (`wp plugin list --status=active`) is a feature-map row: forms, SEO, caching, multilingual, membership, e-commerce. Most have no equivalent and must be rebuilt or dropped with the owner's consent.

## Drupal 8 and later

- **Content:** `node_field_data` (`nid`, `type`, `langcode`, `title`, `status`, `created`, `changed` as Unix seconds); bodies in `node__body` (`entity_id`, `body_value`, `body_format`); other fields in `node__field_<name>` tables. Multilingual sites have one row per `langcode`.
- **Text formats:** `body_format` (`basic_html`, `full_html`, ...) decides which filters ran on output. Render through Drupal if formats do more than pass HTML through.
- **URLs:** `path_alias` (`path` such as `/node/123`, `alias` such as `/about-us`, `langcode`). Build pages at the alias paths; generate rules for `/node/<nid>` if those links circulate.
- **Users:** `users_field_data` (`uid`, `name`, `mail`; never `pass`).
- **Files:** `file_managed` (`uri` such as `public://2019-04/report.pdf`) maps to `sites/default/files/2019-04/report.pdf`.
- **Tools:** `drush sql:query` for exports, `drush cron` for scheduled work.

## Joomla 4 and 5

- **Prefix:** `$dbprefix` in `configuration.php` (often random, such as `abc12_`); the queries below write it as `#__`.
- **Content:** `#__content` (`id`, `title`, `alias`, `introtext`, `fulltext`, `state` where 1 is published, `catid`, `publish_up`, `modified`); categories in `#__categories` (`alias`, `path`).
- **URLs:** search-engine-friendly URLs come from `#__menu` (`path`, and `link` such as `index.php?option=com_content&view=article&id=5`) combined with article aliases; non-SEF URLs are query strings on `/index.php` (mapping page). `#__redirect_links` (`old_url`, `new_url`) holds the site's existing redirects; carry them over.
- **Users:** `#__users` (`name`, `username`, `email`; never `password`).
- **Files:** `images/` by default.

## Magento 2

- **Products are EAV:** `catalog_product_entity` (`entity_id`, `sku`) with values spread over `catalog_product_entity_varchar`, `_int`, `_decimal`, `_text`, `_datetime`, keyed by `attribute_id` (names in `eav_attribute`) and `store_id`. Flatten per store view during export.
- **URLs:** `url_rewrite` (`request_path`, `target_path`, `redirect_type`, `entity_type`, `entity_id`, `store_id`) is the complete URL map: build pages at `request_path` and turn rows with a `redirect_type` into `_redirects` rules.
- **Customers and orders:** `customer_entity` (never `password_hash`), `sales_order`, `sales_order_item`.
- **Commerce is the hard part:** checkout, payments, tax, stock reservations and card-data rules (PCI DSS) need a decision with the owner before any build. A common split is catalog and content on ICP with checkout through an external payment provider; record whichever is chosen in the feature map.
- **Files:** `pub/media/catalog/product/`.
- **Schedules:** `bin/magento cron:run`, with jobs declared in each module's `crontab.xml`.

## Laravel

- **Routes:** `routes/web.php`, `routes/api.php`; list them with `php artisan route:list --json`.
- **Schema:** `database/migrations/` is the most reliable description of the tables; models in `app/Models/`.
- **Views:** Blade templates in `resources/views/` become frontend components or pre-rendered pages.
- **Auth:** the `users` table holds bcrypt hashes (not imported); Sanctum or Passport tokens do not carry over.
- **Schedules and queues:** `php artisan schedule:list` (defined in `app/Console/Kernel.php` up to Laravel 10, `routes/console.php` from Laravel 11); queued jobs (`jobs` table or Redis) become timers or direct calls.
- **Files:** `storage/app/public/` (served through the `public/storage` link).
- **Secrets:** `.env` names only.

## Symfony

- **Routes:** `php bin/console debug:router --format=json`; definitions in `config/routes*` and `#[Route]` attributes on controllers.
- **Schema:** Doctrine entities in `src/Entity/`, migrations in `migrations/`.
- **Views:** Twig templates in `templates/`.
- **Background work:** Messenger handlers and the Scheduler component become timers or direct calls.

## CodeIgniter 4

- **Routes:** `app/Config/Routes.php`; list them with `php spark routes`.
- **Schema:** `app/Database/Migrations/`; models in `app/Models/`; views in `app/Views/`; secrets in `.env`.

## Plain PHP

- `.htaccess` (or the nginx `location` blocks) is the URL map; every rewrite rule is an old-URL test.
- Follow the `include`/`require` graph from each entry file: shared headers, database helpers and global state often hide there.
- SQL embedded in templates is common. Collect every query (`grep -rn -E "SELECT|INSERT|UPDATE|DELETE" --include=*.php`) and give each one a backend method or a reason to drop it.
