--- name: typo3-v11-to-v14-ddev-upgrade description: "Bring an existing TYPO3 v11 site up and booting on TYPO3 v14 LTS (v14.3) locally under DDEV, starting from a v11 SQL dump + the old sitepackage. Use when upgrading/migrating a TYPO3 v11 (or v12/13) project to v14 for local dev, when a v11 database dump must be made to boot on v14, when a t3bootstrap-based v11 site needs its composer/config/schema/upgrade-wizards/site-sets brought to v14, or when after such an upgrade a non-admin editor can't edit content (no edit icons / missing Media tab). Covers: composer.json rework to ^14 + private git repos & composer registry auth, config/system/settings.php, the v14 .htaccess, database:updateschema, backend admin, upgrade wizards (incl. confirmable + one-way ones), language packs, wiring t3bootstrap TypoScript via site sets, fixing non-admin editor permissions (be_groups explicit_allowdeny :ALLOW-format conversion, v14 CType allow-list, non_exclude_fields), and the recurring v11->v14 traps. NOT for fresh installs and NOT for server deployment (see the konsoleH staging-deploy skill for that)." metadata: author: Wappler version: "1.1" --- # TYPO3 v11 → v14 upgrade (local, DDEV) Get an existing **TYPO3 v11 site** installable and **booting on v14 LTS (14.3.x)** under **DDEV**, from a **v11 SQL dump** + the old sitepackage. This is the "make it run" phase — a later phase matches the frontend design (see the *t3bootstrap live-design parity* skill) and another deploys to staging (see the *konsoleH staging-deploy* skill). Work **one step at a time and verify each** before moving on. **`ddev snapshot` before anything destructive** (especially upgrade wizards — several are one-way). Never let a live secret appear in a command. Many errors here look like config bugs but are data/schema/host issues — always confirm the *actual* error before "fixing". ## Placeholders to resolve first - `` — the DDEV project root (docroot `public/`). - `` — the site identifier folder under `config/sites//`. - `` — the site's base host from `config/sites//config.yaml` (e.g. the `*.ddev.site` host). The FE only answers on this host, NOT `localhost`. - `` — the private Composer registry for the theme/vendor packages, if any (e.g. a Gitea `.../api/packages//composer` URL). `` — any `gitea@`/GitHub SSH git repos in `composer.json` `repositories`. - `` — the sitepackage's composer `name` (⚠ often ≠ repo name — read the repo's own `composer.json`). `` — its extension key (keep it stable across the rename). --- ## Step 0 — DDEV up + import the dump (read-only baseline) `ddev start`; import the v11 dump into the `db` database (`ddev import-db --file=dump-*.sql` or `ddev mysql < dump.sql`). Confirm PHP/DB versions match the target (v14.3 wants PHP 8.2–8.5). Snapshot: `ddev snapshot --name pre-upgrade`. ## Step 1 — Rework `composer.json` for v14 - Bump every `typo3/cms-*` to `^14`. Bump third-party exts (news, address, t3bootstrap/*, …) to their ^14 releases. Drop anything with no v14 release (e.g. **flux/vhs are gone in v14 t3bootstrap** — `t3bootstrap/flux-*` do not exist; do not re-add them). - **Custom-repo package name ≠ repo name.** A git repo `…/.git` may publish itself as a *different* composer package name — read the repo's `composer.json` `name` and require THAT. - **Contradictory constraints fail:** `"^14 dev-release/v14"` (tag AND branch) → composer "cannot possibly match". Use plain `"^14"` for a tagged package, or `"dev-"` for an untagged one. - **Repository order controls the source.** If the same package name is served by both a git repo and the registry, composer takes the **first** repo listed as canonical → list the project's git repo **before** the composer registry in `repositories` to make it win. ## Step 2 — Composer auth - SSH git repos: `ddev auth ssh` (loads your keys into the DDEV agent; covers `gitea@…`/GitHub). - Private registry: `ddev composer config http-basic. ` (writes the project `auth.json`). Get the token from the user — never invent/guess it. ## Step 3 — Install `ddev composer update -W` (resolve + install with dependencies). Fix constraint conflicts per Step 1. ## Step 4 — Create `config/system/settings.php` v14 does **not** auto-write `settings.php`; `ConfigurationManager` reads it if present, else falls back to the old `typo3conf/LocalConfiguration.php` in place — so a v11→v14 jump with **production DB creds** in the old file fails to boot first. Create `settings.php` from the old `LocalConfiguration.php`, but with **DDEV DB creds** (host `db`, name `db`, user `db`, pass `db`, port 3306, `utf8mb4`). Preserve `SYS/encryptionKey`, install-tool password, sitename, systemMaintainers, mail sender, and per-extension `EXTENSIONS` config. Drop obsolete v11 feature flags. ## Step 5 — Replace `public/.htaccess` Swap in the v14 template: `cp vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess public/.htaccess`. Symptom of the stale v11 `.htaccess`: Apache **500 "Request exceeded the limit of 10 internal redirects"** (looks like a config error; it's the htaccess). ## Step 6 — Update the DB schema `ddev exec vendor/bin/typo3 database:updateschema "*.add,*.change"` — adds the ~20 tables v12–v14 need (e.g. `sys_csp_resolution`). Non-destructive (add/change only). Symptom if skipped: `Table 'db.sys_csp_resolution' doesn't exist` on any backend request. ## Step 7 — Backend admin `ddev exec vendor/bin/typo3 backend:createadmin ` (or `backend:resetpassword` if an admin already exists in the dump). Do NOT create admin users the user didn't ask for on shared/live systems; local DDEV is fine. ## Step 8 — Upgrade wizards (⚠ snapshot first) `ddev snapshot --name pre-upgradewizards`. Then `vendor/bin/typo3 upgrade:list` and run wizards: - **`upgrade:run` gates on a "Database Up-to-Date" prerequisite** → clear data blockers, re-run `database:updateschema`, then `upgrade:run`. Blockers commonly seen: - `tx_news_domain_model_news.related_links` NULL/non-int → `UPDATE … SET related_links=0 WHERE related_links IS NULL OR NOT related_links REGEXP '^[0-9]+$'`. - `sys_category_record_mm` duplicate PK tuples (table has no `uid`) → add a temp AUTO_INCREMENT PK, `DELETE` via self-join keeping the lowest, drop the temp column. - **Confirmable wizards** (e.g. `convertHeadersize`, `rewriteIconPaths`) error *"You have to acknowledge this wizard"* under `--no-interaction`. There is **no `--confirm` flag** (`--confirm all` throws). Run interactively with piped input: `printf 'yes\n' | ddev exec vendor/bin/typo3 upgrade:run ` (omit `--no-interaction`). - **One-way wizards** (e.g. container migration, address→contacts) — the snapshot is your only undo. - **news 14.0.x vs `FlexFormTools::cleanFlexFormXML()`**: older news calls it with 3 args, v14.3 wants 4 → blocks `txNewsPluginUpdater`. Fix: update news to a patched 14.x, or one-time-patch the vendor `PluginUpdater` (the wizard marks itself done; the patch is then harmless if reverted). ## Step 8b — Language packs `ddev exec vendor/bin/typo3 language:update`. Composer mode ships **no** language packs → labels fall back to English (e.g. news "more-link"). Run this whenever the site language isn't English. ## Step 8c — Non-admin editor permissions (⚠ the migration does NOT fix these) Very confusing symptom: a **non-admin editor** (e.g. a "Redakteur" group) opens the Page module, **sees the content but there are no edit icons**, and/or the **Media tab is missing** from the content form — while the Access (Berechtigungen) module shows the group *has* "edit content" and `tables_modify` includes `tt_content`. Cause: the v11→v14 upgrade wizards do **not** convert the backend group access-lists. Fix three separate `be_groups` fields, per editor group (find it: `SELECT uid,title FROM be_groups WHERE deleted=0`). Editors must **log out/in** afterwards. 1. **`explicit_allowdeny` format — THE big one (blocks ALL content editing).** v≤11 stored allowed values as `tt_content:CType:text:ALLOW`; **v12+ dropped the suffix** and `BackendUserAuthentication:: checkAuthMode()` does an exact `GeneralUtility::inList()` against `tt_content:CType:text` (no suffix). The migrated `:ALLOW` never matches → every CType (even plain Text/Header) is denied → no edit icons on anything. Strip the suffix (all groups): `UPDATE be_groups SET explicit_allowdeny = REPLACE(explicit_allowdeny, ':ALLOW', '') WHERE deleted=0` (`:DENY` entries may stay — `…:value:DENY` never equals `…:value`, so the value correctly stays non-allowed.) Verify: `FIND_IN_SET('tt_content:CType:text', explicit_allowdeny) > 0`. 2. **Missing v14 CType names in the allow-list.** The container migration renamed CTypes `wst3bootstrap_*` → `t3bs_*`; the allow-list only has the old names. Append the new names actually used — compare `SELECT DISTINCT CType FROM tt_content` vs the list and add `tt_content:CType:` (NO `:ALLOW` suffix) for each missing one; typically `t3bs_fluidrow/_column/_tabs/_tab_item/_accordion/ _accordion_item/_megamenu`, `ws_slider`, `heroitem`, `news_*`, `address_*`. 3. **`non_exclude_fields` missing v14 fields → hidden tabs (e.g. Media).** TCA `exclude` fields are hidden from non-admins unless granted here (format `table:field`, unchanged in v12). The v11 list lacks fields ADDED in v14, so any tab whose fields are ALL exclude+denied won't render — e.g. the hero **Media** tab contains only `tx_heroitem_bg`/`_bg_tablet`/`_bg_smartphone` (all denied → no tab). Grant the missing `tt_content` exclude fields (append `tt_content:` for each). Common culprits: `imagecols_grid, effects, aos_*, bg_container, tx_wsslider_{renderer,layout,source}, tx_heroitem_*, tx_teaser2_layout`. NOTE `image`/`assets`/`media` are NOT exclude fields (always shown) — the missing tab is the *settings* fields, not the FAL field. Other record types editors touch (news, address) can be stale the same way. **Diagnose precisely — read-only CLI bootstrap** (also the way to *confirm* a fix without a BE login): ```php SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_CLI | SystemEnvironmentBuilder::REQUESTTYPE_BE); Bootstrap::init($classLoader); $be = GeneralUtility::makeInstance(BackendUserAuthentication::class); $be->user = BackendUtility::getRecord('be_users', ); $be->fetchGroupData(); $GLOBALS['BE_USER'] = $be; $be->recordEditAccessInternals('tt_content', $row, false, false, true); // false → $be->errorMsg names the failing gate $be->check('non_exclude_fields', 'tt_content:'); // tab/field visibility $be->doesUserHaveAccess($pageRow, 16); // page "edit content" perm ``` `recordEditAccessInternals` failing with `authMode "explicitAllow" failed for field "CType"` == facet 1/2. (Writing `be_groups` *from* such a script may be blocked by agent tooling — apply fixes as plain SQL `UPDATE`/`REPLACE`/`CONCAT`.) Red herring: an empty `db_mountpoints` on the user is fine if the group (or a subgroup) supplies a mount — check the user's *effective* `getWebmounts()` before chasing it. ## Step 9 — Wire TypoScript via site sets v14 t3bootstrap delivers TypoScript (incl. the `PAGE` object) through **site sets**, not `sys_template`. The site config must declare them under `dependencies:` in `config/sites//config.yaml`, or the FE dies with **"No page configured for type=0"**. Add the set identifiers (the `name:` in each ext's `Configuration/Sets/*/config.yaml` — independent of package name/extkey), e.g. the theme set + container-bs5-templates + news/form/address/blog/slider as used. See the design skill for details. ## Step 10 — Flush + verify `ddev exec vendor/bin/typo3 cache:flush`. Verify over HTTP with the **correct host**: `/typo3/` → 200, FE with `curl -H "Host: " …` (or the real DDEV URL) → 200. **"No site configuration found" is usually a false alarm from curling `localhost`** instead of ``. --- ## Other recurring traps - **Leftover custom content element with no v14 provider → FE fatal** empty-`templateName` `InvalidTemplateResourceException`. Don't assume it's flux — check the old sitepackage; a plain custom FSC CType must be re-registered for v14 (see the design skill's iconmenu example). - **Relocating a git-repo package's vendor path:** change the `name` in *that repo's own* `composer.json` (composer installs by declared name, not repo URL). Keep the `extension-key` unchanged so `EXT:/…` refs + set names keep working. Untagged repo → require `dev-`. - **v14 FlexForm relational fields:** `FlexFormFieldValues::get()` returns a `LazyRecordCollection` / `RecordInterface` for relation fields (NOT a CSV of uids), which breaks `intExplode()`/`(int)` casts in migrated/old ext code → `TypeError`. Normalize to `(string)`/`(string)$record->getUid()` before parsing. Reproduce/verify from CLI with a small bootstrap script (SystemEnvironmentBuilder::run + Bootstrap::init + the record API). - **TYPO3 Console** (`helhum/typo3-console` ^8.3) works with v14 — `vendor/bin/typo3` is the console binary. Each step verified + a snapshot before wizards = a reversible, debuggable upgrade.