Files
t3bootstrap-upgrade-skills/typo3-v11-to-v14-ddev-upgrade/SKILL.md
T
melnicenkoandClaude Opus 5 c1e3a059d3 Verallgemeinerte Erkenntnisse aus einem Ticketdurchlauf einarbeiten
Design-Paritaet 1.5 -> 1.6:
- Site-Set-page.tsconfig erreicht keine Datensaetze in Ordnern neben dem
  Site-Root; solche Konfiguration gehoert in die globale page.tsconfig
  der Extension. Symptom ist, dass eine TCEFORM-Aenderung genau fuer
  einen Datensatztyp wirkungslos bleibt.
- Angebotene und gerenderte Crop-Variante koennen auseinanderlaufen,
  dann beschneidet die Redaktion ins Leere.
- Feste Bildhoehe laesst das Seitenverhaeltnis ueber die Breakpoints
  wandern, sodass kein Zuschnitt mehr ueberall passt; aspect-ratio statt
  fester Hoehe.
- Umbrechende Labels brauchen eine reservierte Zeilenhoehe, sonst
  springt der Rhythmus des Rasters; dabei die Untermarge des Absatzes
  neutralisieren und die schmalste Geraetebreite pruefen, nicht nur 360.
- :has() laesst sich nicht in :has() verschachteln, die Regel wird
  komplett verworfen - ohne jede Fehlermeldung.
- Neuer Abschnitt zu Meldungen, die Rasterartefakte statt Fehler sind:
  gegen die Referenzseite pruefen, ueber mehrere Zoomstufen UND
  Subpixel-Versaetze messen, das Ganze per Canvas in einem Durchgang -
  und rechtzeitig sagen, dass Geometrie es nicht loest.

v11->v14 1.1 -> 1.2:
- Ein "verlorenes" Feld ist oft ein umgebautes Feld; zwei Spalten werden
  gern zu einem Feld plus Schalter. Erst das neue Modell pruefen, bevor
  man eine fremde Extension patcht - dann ist es eine Datenmigration.
  Dazu die abgesicherte UPDATE-Bedingung und der Hinweis, dass Fluid
  keinen !-Operator kennt.
- Vor "Regression" gegen die Referenzseite pruefen: zwei Tickets zum
  selben fehlenden Element koennen Verlust und Neuwunsch sein.

Cutover 1.1 -> 1.2:
- Eingebettete Fremddienste sperren sich per frame-ancestors oft gegen
  Staging. Solche Tests sind dort in keinem Browser moeglich; frueh
  klaeren, den eigenen Anteil trotzdem pruefen.

Redmine 1.3 -> 1.4:
- Textile beendet den Durchstrich am ersten Bindestrich im Text, ein
  Pfeil laesst also die Handlungsaufforderung ungestrichen stehen; HTML
  hilft nicht, Redmine escapt es. Und das Ergebnis nachsehen, die API
  bestaetigt nur die Speicherung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:05:42 +02:00

15 KiB
Raw Blame History

name, description, metadata
name description metadata
typo3-v11-to-v14-ddev-upgrade 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).
author version
Wappler 1.2

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

  • <PROJECT_DIR> — the DDEV project root (docroot public/).
  • <SITE_ID> — the site identifier folder under config/sites/<SITE_ID>/.
  • <SITE_BASE_HOST> — the site's base host from config/sites/<SITE_ID>/config.yaml (e.g. the *.ddev.site host). The FE only answers on this host, NOT localhost.
  • <REGISTRY_URL> — the private Composer registry for the theme/vendor packages, if any (e.g. a Gitea .../api/packages/<vendor>/composer URL). <GIT_REPOS> — any gitea@/GitHub SSH git repos in composer.json repositories.
  • <TEMPLATE_PACKAGE> — the sitepackage's composer name (⚠ often ≠ repo name — read the repo's own composer.json). <TEMPLATE_EXTKEY> — 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.28.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 t3bootstrapt3bootstrap/flux-* do not exist; do not re-add them).
  • Custom-repo package name ≠ repo name. A git repo …/<name>.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-<branch>" 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.<registry-host> <user> <token> (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 v12v14 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 <user> <pass> (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 <id> (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:<name> (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:<field> 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):

SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_CLI | SystemEnvironmentBuilder::REQUESTTYPE_BE);
Bootstrap::init($classLoader);
$be = GeneralUtility::makeInstance(BackendUserAuthentication::class);
$be->user = BackendUtility::getRecord('be_users', <uid>); $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:<field>');                  // 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/<SITE_ID>/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: <SITE_BASE_HOST>" … (or the real DDEV URL) → 200. "No site configuration found" is usually a false alarm from curling localhost instead of <SITE_BASE_HOST>.


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:<key>/… refs + set names keep working. Untagged repo → require dev-<branch>.
  • 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.
  • ⚠ A "lost" field is often a REMODELLED field — check the new data model before patching anything. Editors report that something they used to maintain has disappeared, you find the old column still sitting in the DB with data and no v14 code referencing it, and the obvious conclusion is that the extension dropped the feature and needs an upstream fix. Look for the v14 equivalent first: two v11 columns are frequently folded into one field plus a switch (real case: separate columns for a pre- and post-nominal academic title became one title field plus an "append" boolean). Then it is a data migration, not a code change — and no release of a shared extension is needed.
    • Guard the migration so it cannot destroy the other use of the surviving field: SET <new> = <old>, <flag> = 1 WHERE <old> <> '' AND (<new> = '' OR <new> IS NULL), and verify afterwards that no row was touched which should not have been.
    • Then check whether anything RENDERS the switch. A flag that no template evaluates — not even the owning extension's own templates — is exactly why the data looked lost. Grep the templates for the property before assuming the display side works.
    • Fluid has no ! operator: write {x} && {flag} == 0 / == 1, not !{flag}.
  • ⚠ Verify against the reference site before calling something a regression. Two tickets can describe the same missing element and be different things: on one page the old site really did show it (a migration loss), on another it never did (a new wish). Fetch the live page and count the occurrences. It changes what you promise, and it saves the "why does it look different from before" conversation later.

Each step verified + a snapshot before wizards = a reversible, debuggable upgrade.