
Typo3 Extension Upgrade
- 91 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
Upgrade a TYPO3 extension to a newer LTS using a staged Rector, Fractor, PHPStan, and PHPUnit workflow with dependency auditing.
About
This skill provides a systematic framework for upgrading TYPO3 extensions to newer LTS versions, fixing deprecated APIs and compatibility issues. A developer uses it when migrating an extension from v11/v12/v13 toward the v14.3 LTS target.
- Systematic upgrade workflow using Extension Scanner, Rector, Fractor, and PHPStan
- Audits third-party dependencies and tests across each supported TYPO3 version
Typo3 Extension Upgrade by the numbers
- 91 all-time installs (skills.sh)
- Ranked #115 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill typo3-extension-upgradeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 91 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Upgrade a TYPO3 extension to a newer LTS using a staged Rector, Fractor, PHPStan, and PHPUnit workflow with dependency auditing.
Files
TYPO3 Extension Upgrade Skill
Systematic framework for upgrading TYPO3 extensions to newer LTS versions. Extension code only -- NOT for project/core upgrades.
Upgrade Toolkit
| Tool | Purpose | Files |
|---|---|---|
| Extension Scanner | Diagnose deprecated APIs | TYPO3 Backend |
| Rector | Automated PHP migrations | .php |
| Fractor | Non-PHP migrations | FlexForms, TypoScript, YAML, Fluid |
| PHPStan | Static analysis | .php |
Core Workflow
1. Complete planning phase (consult references/pre-upgrade.md) 2. Create feature branch (verify git is clean) 3. Update composer.json constraints for target version 4. Audit third-party dependencies for major version changes (consult references/third-party-dependency-upgrades.md) 5. Run rector process --dry-run then review and apply 6. Run fractor process --dry-run then review and apply 7. Run php-cs-fixer fix 8. Run phpstan analyse against each supported dependency version and fix errors 9. Run phpunit and fix tests 10. Test in target TYPO3 version(s) 11. Verify success criteria (consult references/verification.md)
When NOT to Apply Automatically
Do NOT blindly apply Rector/Fractor if dual-version compatibility is needed, tests are missing, changes are unclear, or complex APIs (DBAL, Extbase) are affected. Instead: apply specific rules manually, test between each change.
Third-Party Dependency Upgrades
When composer.json widens constraints to a new major version: enumerate API usages, cross-reference the new version's API, flag interface/concrete-only methods, verify mocks against all supported versions, use adapter pattern for signature differences, run PHPStan against each major version. See references/third-party-dependency-upgrades.md.
Quick Commands
rector process --dry-run && rector process # PHP migrations
fractor process --dry-run && fractor process # Non-PHP migrations
php-cs-fixer fix && phpstan analyse && phpunit # Quality checksAsset Templates
Config templates in assets/: rector.php, fractor.php, phpstan.neon, phpunit.xml, .php-cs-fixer.php
References
| Reference | Use when... |
|---|---|
references/pre-upgrade.md | Starting an upgrade: planning checklist, version audit, risk assessment |
references/api-changes.md | Checking deprecated/removed APIs by TYPO3 version |
references/api-traps.md | Cross-version footguns: TCA restrictions, boot order, DI bypass |
references/upgrade-v11-to-v12.md | Upgrading from TYPO3 v11 to v12 |
references/upgrade-v12-to-v13.md | Upgrading from TYPO3 v12 to v13 |
references/upgrade-v13-to-v14.md | Upgrading from TYPO3 v13 to v14 |
references/dual-compatibility.md | Maintaining dual compatibility (v12 + v13) |
references/real-world-patterns.md | Looking for real-world migration examples |
references/toolchain-output.md | Understanding Rector/Fractor dry-run output |
references/troubleshooting.md | Rector broke code, PHPStan errors, test failures |
references/third-party-dependency-upgrades.md | Upgrading non-TYPO3 dependencies (major version bumps, adapter patterns) |
references/verification.md | Checking success criteria and real-world testing |
references/multi-version-worktrees.md | Per-LTS worktree layout, backport workflow, cross-version CI matrix |
External Resources
---
Credits & Attribution
This skill is based on the excellent work by [Netresearch DTT GmbH](https://www.netresearch.de/).
Original repository: https://github.com/netresearch/typo3-extension-upgrade-skill
Copyright (c) Netresearch DTT GmbH — Methodology and best practices (MIT / CC-BY-SA-4.0)
Special thanks to Netresearch DTT GmbH for their generous open-source contributions to the TYPO3 community, which helped shape this skill collection. Adapted by webconsulting.at for this skill collection
<?php
/**
* PHP CS Fixer configuration for TYPO3 extensions
*
* Based on TYPO3 coding standards with modern PHP 8.2+ support
*
* Usage:
* ./vendor/bin/php-cs-fixer fix --dry-run --diff # Preview
* ./vendor/bin/php-cs-fixer fix # Apply
*/
declare(strict_types=1);
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude([
'.Build',
'.github',
'vendor',
'node_modules',
])
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PER-CS2.0' => true,
'@PER-CS2.0:risky' => true,
'@PHP82Migration' => true,
// Array notation
'array_syntax' => ['syntax' => 'short'],
'no_whitespace_before_comma_in_array' => true,
'whitespace_after_comma_in_array' => true,
'trim_array_spaces' => true,
'normalize_index_brace' => true,
// Casing
'class_attributes_separation' => [
'elements' => [
'const' => 'one',
'method' => 'one',
'property' => 'one',
],
],
// Cast notation
'cast_spaces' => ['space' => 'single'],
'no_short_bool_cast' => true,
// Class notation
'no_null_property_initialization' => true,
'ordered_class_elements' => [
'order' => [
'use_trait',
'constant_public',
'constant_protected',
'constant_private',
'property_public',
'property_protected',
'property_private',
'construct',
'destruct',
'magic',
'phpunit',
'method_public',
'method_protected',
'method_private',
],
],
'self_accessor' => true,
'single_class_element_per_statement' => true,
// Control structure
'no_superfluous_elseif' => true,
'no_useless_else' => true,
'simplified_if_return' => true,
'trailing_comma_in_multiline' => [
'elements' => ['arguments', 'arrays', 'match', 'parameters'],
],
// Function notation
'function_declaration' => ['closure_function_spacing' => 'one'],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
],
'nullable_type_declaration_for_default_null_value' => true,
'return_type_declaration' => ['space_before' => 'none'],
'single_line_throw' => false,
// Import
'fully_qualified_strict_types' => true,
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => false,
'import_functions' => false,
],
'no_unused_imports' => true,
'ordered_imports' => [
'imports_order' => ['class', 'function', 'const'],
'sort_algorithm' => 'alpha',
],
// Language construct
'declare_equal_normalize' => ['space' => 'none'],
'declare_parentheses' => true,
'single_space_around_construct' => true,
// Namespace notation
'no_leading_namespace_whitespace' => true,
// Operator
'binary_operator_spaces' => ['default' => 'single_space'],
'concat_space' => ['spacing' => 'one'],
'not_operator_with_successor_space' => false,
'operator_linebreak' => ['only_booleans' => true],
'unary_operator_spaces' => true,
// PHPDoc
'general_phpdoc_tag_rename' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_phpdoc' => true,
'no_superfluous_phpdoc_tags' => [
'allow_mixed' => true,
'remove_inheritdoc' => true,
],
'phpdoc_align' => ['align' => 'left'],
'phpdoc_indent' => true,
'phpdoc_line_span' => [
'const' => 'single',
'method' => 'multi',
'property' => 'single',
],
'phpdoc_no_empty_return' => true,
'phpdoc_order' => true,
'phpdoc_scalar' => true,
'phpdoc_separation' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => true,
'phpdoc_to_comment' => false,
'phpdoc_trim' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_types' => true,
'phpdoc_types_order' => [
'null_adjustment' => 'always_last',
'sort_algorithm' => 'none',
],
'phpdoc_var_without_name' => true,
// Return notation
'no_useless_return' => true,
'return_assignment' => true,
'simplified_null_return' => true,
// Semicolon
'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'],
'no_empty_statement' => true,
'no_singleline_whitespace_before_semicolons' => true,
// Strict
'declare_strict_types' => true,
'strict_comparison' => true,
'strict_param' => true,
// String notation
'explicit_string_variable' => true,
'single_quote' => true,
// Whitespace
'array_indentation' => true,
'blank_line_before_statement' => [
'statements' => [
'break',
'continue',
'declare',
'return',
'throw',
'try',
],
],
'method_chaining_indentation' => true,
'no_extra_blank_lines' => [
'tokens' => [
'case',
'continue',
'curly_brace_block',
'default',
'extra',
'parenthesis_brace_block',
'square_brace_block',
'switch',
'throw',
'use',
],
],
'no_spaces_around_offset' => true,
])
->setFinder($finder);
<?php
/**
* Fractor configuration for TYPO3 extension upgrade
*
* Fractor handles non-PHP file migrations:
* - FlexForms (XML)
* - TypoScript
* - YAML (e.g., Services.yaml)
* - Fluid templates
* - .htaccess files
*
* Usage:
* ./vendor/bin/fractor process --dry-run # Preview changes
* ./vendor/bin/fractor process # Apply changes
*
* @see https://github.com/andreaswolf/fractor
*/
declare(strict_types=1);
use a9f\Fractor\Configuration\FractorConfiguration;
use a9f\Typo3Fractor\Set\Typo3LevelSetList;
return FractorConfiguration::configure()
->withPaths([
__DIR__ . '/Configuration',
__DIR__ . '/Resources',
])
->withSkip([
__DIR__ . '/.Build',
__DIR__ . '/vendor',
])
->withSets([
// TYPO3 v12 migrations
// Handles FlexForm, TypoScript, YAML, Fluid migrations
Typo3LevelSetList::UP_TO_TYPO3_12,
// Uncomment for v13 if only supporting v13+
// Typo3LevelSetList::UP_TO_TYPO3_13,
]);
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
parameters:
# Level 8 is recommended for TYPO3 extensions
# Adjust based on project maturity
level: 8
paths:
- Classes
- Tests
excludePaths:
- .Build
- vendor
# Don't treat PHPDoc types as certain - allows gradual typing
treatPhpDocTypesAsCertain: false
# Ignore specific errors if needed
ignoreErrors:
# Example: Ignore specific deprecation warnings during migration
# - '#Call to deprecated method#'
# TYPO3-specific settings
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
colors="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>Tests/Unit</directory>
</testsuite>
<testsuite name="Functional">
<directory>Tests/Functional</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">Classes</directory>
</include>
<exclude>
<directory>Classes/Domain/Model</directory>
</exclude>
</source>
<coverage>
<report>
<html outputDirectory=".Build/coverage"/>
<clover outputFile=".Build/coverage/clover.xml"/>
</report>
</coverage>
<php>
<!-- TYPO3 testing framework settings -->
<env name="TYPO3_PATH_ROOT" value=".Build/public"/>
<env name="TYPO3_PATH_WEB" value=".Build/public"/>
<ini name="display_errors" value="1"/>
<ini name="error_reporting" value="E_ALL"/>
</php>
</phpunit>
<?php
/**
* Rector configuration for TYPO3 v12/v13 extension upgrade
*
* Usage:
* ./vendor/bin/rector process --dry-run # Preview changes
* ./vendor/bin/rector process # Apply changes
*/
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
use Rector\Set\ValueObject\LevelSetList;
use Ssch\TYPO3Rector\Set\Typo3LevelSetList;
use Ssch\TYPO3Rector\Set\Typo3SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/Classes',
__DIR__ . '/Configuration',
__DIR__ . '/Tests',
]);
$rectorConfig->skip([
__DIR__ . '/ext_emconf.php',
__DIR__ . '/.Build',
__DIR__ . '/vendor',
]);
$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');
$rectorConfig->importNames();
$rectorConfig->removeUnusedImports();
// Define what rule sets will be applied
$rectorConfig->sets([
// PHP level upgrades to 8.2
LevelSetList::UP_TO_PHP_82,
// TYPO3 v12 migrations only
// IMPORTANT: Don't use UP_TO_TYPO3_13 if extension supports both ^12.4 || ^13.4
// v13 rules introduce v13-only APIs that break v12 compatibility:
// - $GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.user')
// - $GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.page.information')
Typo3LevelSetList::UP_TO_TYPO3_12,
// TYPO3 code quality and general improvements
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
]);
// Skip rules that may cause issues or require manual review
$rectorConfig->skip([
// Skip constructor promotion - keep explicit property declarations for clarity
ClassPropertyAssignToConstructorPromotionRector::class,
// Skip removing parent calls - may be needed for TYPO3 hooks
RemoveParentCallWithoutParentRector::class,
]);
};
# Checkpoints for typo3-extension-upgrade skill
# Validates extension readiness for TYPO3 version upgrades
# RELOCATED FROM php-modernization-skill (v1.16.0 refocus):
# PM-35 -> TU-54 (mechanical: SingletonInterface in Classes/src)
# PM-36 -> TU-55 (LLM: SingletonInterface migration to DI scoping)
version: 1
skill_id: typo3-extension-upgrade
mechanical:
# === RECTOR CONFIGURATION ===
# Accept both root and Build/ subdirectory layouts (common in extensions that
# keep testing/CI tooling under Build/). Build/rector/rector.php is the
# Netresearch default when using a split Build/ structure.
#
# All checks here use `command` checkpoints that loop over candidate paths
# and only `grep` files that actually exist — `grep -l a b c` exits with
# status 2 when any listed file is missing, which would otherwise mask real
# matches in other candidates. The runner's brace-expansion in `file_exists`
# is used for TU-01/TU-04 since those are pure existence checks.
- id: TU-01
type: file_exists
target: "{rector.php,Build/rector.php,Build/rector/rector.php}"
severity: warning
desc: "rector.php should exist for automated code migration (root or Build/ subdir)"
- id: TU-02
type: command
pattern: 'for f in rector.php Build/rector.php Build/rector/rector.php; do [ -f "$f" ] && grep -q "Typo3LevelSetList" "$f" && exit 0; done; exit 1'
severity: warning
desc: "Rector config should use TYPO3 level sets for version upgrades"
- id: TU-03
type: command
pattern: 'for f in rector.php Build/rector.php Build/rector/rector.php; do [ -f "$f" ] && grep -q "RectorConfig" "$f" && exit 0; done; exit 1'
severity: warning
desc: "Rector config should use modern RectorConfig format"
# === FRACTOR CONFIGURATION (TypoScript/Fluid) ===
- id: TU-04
type: file_exists
target: "{fractor.php,Build/fractor.php,Build/fractor/fractor.php}"
severity: info
desc: "fractor.php should exist for TypoScript/Fluid migration (root or Build/ subdir)"
- id: TU-05
type: command
pattern: 'for f in fractor.php Build/fractor.php Build/fractor/fractor.php; do [ -f "$f" ] && grep -q "Typo3LevelSetList" "$f" && exit 0; done; exit 1'
severity: info
desc: "Fractor config should use TYPO3 level sets"
# === COMPOSER.JSON VERSION CONSTRAINTS ===
- id: TU-06
type: json_path
target: composer.json
pattern: '.require["php"]'
severity: error
desc: "composer.json must declare PHP version constraint"
- id: TU-07
type: json_path
target: composer.json
pattern: '.require["typo3/cms-core"]'
severity: error
desc: "composer.json must declare typo3/cms-core constraint"
- id: TU-08
type: regex
target: composer.json
pattern: '"php"\s*:\s*"[^"]*8\.[0-9]'
severity: warning
desc: "PHP constraint should include PHP 8.x versions"
- id: TU-09
type: regex
target: composer.json
pattern: '"typo3/cms-core"\s*:\s*"[^"]*1[23]\.'
severity: warning
desc: "TYPO3 constraint should include v12 or v13"
# === DEPRECATED API DETECTION ===
- id: TU-10
type: not_contains
target: "Classes/**/*.php"
pattern: "GeneralUtility::devLog"
severity: error
desc: "Must not use deprecated GeneralUtility::devLog (removed in v10)"
- id: TU-11
type: not_contains
target: "Classes/**/*.php"
pattern: "$GLOBALS['TYPO3_DB']"
severity: error
desc: "Must not use deprecated $GLOBALS['TYPO3_DB'] (removed in v9)"
- id: TU-12
type: not_contains
target: "Classes/**/*.php"
pattern: "piVars"
severity: warning
desc: "Should not use deprecated piVars (pi_base pattern)"
- id: TU-13
type: not_contains
target: "Classes/**/*.php"
pattern: "AbstractPlugin"
severity: warning
desc: "Should not use deprecated AbstractPlugin (pi_base)"
- id: TU-14
type: not_contains
target: "Classes/**/*.php"
pattern: "TYPO3_MODE"
severity: error
desc: "Must not use deprecated TYPO3_MODE constant (removed in v11)"
- id: TU-15
type: not_contains
target: "Classes/**/*.php"
pattern: "ObjectManager::getInstance"
severity: warning
desc: "Should not use ObjectManager::getInstance (use DI)"
- id: TU-16
type: not_contains
target: "Classes/**/*.php"
pattern: "extRelPath"
severity: error
desc: "Must not use deprecated extRelPath (removed in v9)"
- id: TU-17
type: not_contains
target: "Classes/**/*.php"
pattern: "PATH_site"
severity: error
desc: "Must not use deprecated PATH_site constant (use Environment API)"
- id: TU-18
type: not_contains
target: "Classes/**/*.php"
pattern: "PATH_typo3conf"
severity: error
desc: "Must not use deprecated PATH_typo3conf constant"
# === EXT_TABLES.PHP / EXT_LOCALCONF.PHP ===
- id: TU-19
type: not_contains
target: ext_tables.php
pattern: "ExtensionManagementUtility::addModule"
severity: warning
desc: "Should use Configuration/Backend/Modules.php instead of addModule"
- id: TU-20
type: not_contains
target: ext_tables.php
pattern: "registerPlugin"
severity: warning
desc: "Plugins should be registered via Configuration/TCA/Overrides/"
# === TYPOSCRIPT CHECKS ===
- id: TU-21
type: not_contains
target: "**/*.typoscript"
pattern: "CONTENT"
severity: info
desc: "Consider migrating CONTENT objects to Fluid"
- id: TU-22
type: not_contains
target: "**/*.typoscript"
pattern: "USER_INT"
severity: info
desc: "Review USER_INT usage for caching implications"
# === FLUID TEMPLATE CHECKS ===
- id: TU-23
type: not_contains
target: "**/*.html"
pattern: "f:widget"
severity: error
desc: "Must not use deprecated f:widget (removed in v12)"
- id: TU-24
type: not_contains
target: "**/*.html"
pattern: "f:be.container"
severity: warning
desc: "Should migrate f:be.container to module template"
# === EXTENSION SCANNER INDICATORS ===
- id: TU-25
type: not_contains
target: "Classes/**/*.php"
pattern: "@extensionScannerIgnoreLine"
severity: info
desc: "Review extensionScannerIgnoreLine annotations"
# === SERVICES.YAML FOR DI ===
- id: TU-26
type: file_exists
target: Configuration/Services.yaml
severity: warning
desc: "Services.yaml should exist for dependency injection"
- id: TU-27
type: contains
target: Configuration/Services.yaml
pattern: "autowire: true"
severity: info
desc: "Services.yaml should enable autowiring"
# === MODERN BACKEND MODULE REGISTRATION ===
- id: TU-28
type: file_exists
target: Configuration/Backend/Modules.php
severity: info
scope: backend
desc: "Backend modules should use Configuration/Backend/Modules.php (v12+)"
# === TCA MIGRATION ===
- id: TU-29
type: not_contains
target: "Configuration/TCA/**/*.php"
pattern: "'type' => 'input'"
severity: info
desc: "Review TCA type=input for migration to specific types (email, link, etc.)"
- id: TU-30
type: not_contains
target: "Configuration/TCA/**/*.php"
pattern: "'renderType' => 'inputLink'"
severity: warning
desc: "Should migrate inputLink to TCA type=link (v12+)"
# === DEPRECATED REQUEST ACCESSORS ===
- id: TU-31
type: not_contains
target: "Classes/**/*.php"
pattern: "GeneralUtility::_GP("
severity: warning
desc: "Should not use deprecated GeneralUtility::_GP(); use PSR-7 request instead"
- id: TU-32
type: not_contains
target: "Classes/**/*.php"
pattern: "GeneralUtility::_GET("
severity: warning
desc: "Should not use deprecated GeneralUtility::_GET(); use PSR-7 request instead"
- id: TU-33
type: not_contains
target: "Classes/**/*.php"
pattern: "GeneralUtility::_POST("
severity: warning
desc: "Should not use deprecated GeneralUtility::_POST(); use PSR-7 request instead"
- id: TU-34
type: not_contains
target: "Classes/**/*.php"
pattern: "$GLOBALS['TSFE']"
severity: warning
desc: "Should not access TSFE via $GLOBALS; use PSR-7 request attributes or Context API"
# === DEPRECATED DI PATTERNS ===
- id: TU-35
type: not_contains
target: "Classes/**/*.php"
pattern: "@inject"
severity: warning
desc: "Should not use deprecated @inject annotation; use constructor injection"
# === DEPRECATED HOOK REGISTRATIONS ===
- id: TU-36
type: not_contains
target: ext_localconf.php
pattern: "SignalSlot"
severity: warning
desc: "Should not use deprecated Signal/Slot; migrate to PSR-14 events"
# === THIRD-PARTY DEPENDENCY VERSION COMPATIBILITY ===
- id: TU-60
type: command
pattern: "php -r \"\\$c=json_decode(file_get_contents('composer.json'),true);foreach(\\$c['require']??[] as \\$p=>\\$v){if(preg_match('/\\|\\|/',\\$v)&&\\$p!=='php'&&!str_starts_with(\\$p,'typo3/')&&!str_starts_with(\\$p,'ext-')&&!str_starts_with(\\$p,'lib-'))echo \\$p.': '.\\$v.PHP_EOL;}\" 2>/dev/null"
severity: info
desc: "Identify non-TYPO3 dependencies with multi-major-version constraints (|| operator)"
- id: TU-61
type: not_contains
target: "Classes/**/*.php"
pattern: "@phpstan-ignore method.notFound"
severity: warning
desc: "Should not suppress method.notFound via @phpstan-ignore — use adapter pattern for version-conditional code"
- id: TU-62
type: not_contains
target: "Classes/**/*.php"
pattern: "@phpstan-ignore-next-line"
severity: info
desc: "Review @phpstan-ignore-next-line annotations — may mask version-specific runtime errors"
# === PHP-CS-FIXER CONFIGURATION ===
- id: TU-37
type: command
pattern: "ls Build/php-cs-fixer/php-cs-fixer.php .php-cs-fixer.php .php-cs-fixer.dist.php 2>/dev/null | head -1 || echo 'NOT_FOUND'"
severity: info
desc: "PHP-CS-Fixer configuration should exist for code style enforcement"
# === CHANGELOG ===
- id: TU-38
type: file_exists
target: CHANGELOG.md
severity: info
desc: "CHANGELOG.md should exist to document version changes"
# === DEPRECATED CONSTANTS ===
- id: TU-39
type: not_contains
target: "Classes/**/*.php"
pattern: "TYPO3_REQUESTTYPE"
severity: warning
desc: "Should not use deprecated TYPO3_REQUESTTYPE constant (use ApplicationType)"
# === SINGLETONINTERFACE DEPRECATION (TYPO3 v14) ===
- id: TU-54
type: command
pattern: 'for d in Classes src; do [ -d "$d" ] && grep -rql "SingletonInterface" "$d" 2>/dev/null && exit 1; done; exit 0'
severity: warning
desc: "SingletonInterface is deprecated in TYPO3 v14. Migrate to proper DI scoping (Services.yaml shared: true) unless the class must be used with GeneralUtility::setSingletonInstance in tests."
llm_reviews:
# === COMPREHENSIVE RECTOR REVIEW ===
- id: TU-40
domain: code-migration
prompt: |
Review the rector.php configuration for TYPO3 extension upgrade readiness:
1. Check if appropriate TYPO3 LevelSetLists are configured (e.g., Typo3LevelSetList::UP_TO_TYPO3_12)
2. Verify PHP upgrade sets are included if needed (e.g., PHP 8.1, 8.2, 8.3, 8.4)
3. Check if paths are correctly configured to scan Classes/, Configuration/, etc.
4. Look for any custom rectors that might conflict with TYPO3 upgrades
5. Verify skip patterns don't exclude important directories
Report any missing configurations or improvements needed.
severity: warning
desc: "Comprehensive Rector configuration review"
# === FRACTOR REVIEW ===
- id: TU-41
domain: code-migration
prompt: |
Review the fractor.php configuration if present:
1. Check if TYPO3 TypoScript migration rules are enabled
2. Verify Fluid template migration rules are configured
3. Check if paths include all TypoScript and Fluid directories
4. Review any skip patterns
If fractor.php doesn't exist, recommend creating one for TypoScript/Fluid migrations.
severity: info
desc: "Fractor configuration for TypoScript/Fluid migration"
# === COMPOSER VERSION STRATEGY ===
- id: TU-42
domain: version-compatibility
prompt: |
Analyze composer.json version constraints:
1. Check PHP version range - should support current PHP versions (8.2-8.4)
2. Check TYPO3 version range - should specify clear major version constraints
3. Verify the PHP and TYPO3 constraints are compatible with each other
4. Check require-dev for testing tools (phpunit, phpstan) version compatibility
5. Look for any packages that might block upgrades (abandoned, unmaintained)
Report version constraint issues and recommendations.
severity: error
desc: "Version constraint compatibility analysis"
# === DEPRECATED API DEEP SCAN ===
- id: TU-43
domain: api-compatibility
prompt: |
Scan PHP code for deprecated TYPO3 API usage beyond simple pattern matching:
1. Look for deprecated hook registrations (TYPO3_CONF_VARS hooks removed in recent versions)
2. Check for deprecated class inheritance (AbstractPlugin, AbstractController patterns)
3. Identify deprecated service instantiation patterns (makeInstance vs DI)
4. Look for deprecated exception classes
5. Check for deprecated annotation usage (@inject vs constructor injection)
6. Identify XCLASSing that might break with upgrades
Provide specific file:line references for each issue found.
severity: error
desc: "Deep scan for deprecated TYPO3 API usage"
# === TCA MIGRATION ANALYSIS ===
- id: TU-44
domain: configuration
prompt: |
Analyze TCA configuration for upgrade compatibility:
1. Check for deprecated TCA type configurations
2. Identify renderType deprecations (inputLink -> type=link, etc.)
3. Look for deprecated eval configurations
4. Check for wizard configurations that need migration
5. Verify ctrl section has required fields for target version
6. Check for deprecated palettes or showitem syntax
Report specific migration needs with before/after examples.
severity: warning
desc: "TCA configuration migration analysis"
# === EXTENSION SCANNER SIMULATION ===
- id: TU-45
domain: api-compatibility
prompt: |
Simulate Extension Scanner checks:
1. List all @extensionScannerIgnoreLine annotations and verify they're still needed
2. Check for strong deprecation patterns that Extension Scanner would flag
3. Identify breaking changes between current TYPO3 version and target version
4. Look for calls to internal TYPO3 APIs that might change
5. Check for database schema changes that need migration
Categorize findings by severity (breaking, deprecated, needs-review).
severity: error
desc: "Extension Scanner compatibility simulation"
# === TYPOSCRIPT MODERNIZATION ===
- id: TU-46
domain: configuration
prompt: |
Review TypoScript configuration for modernization:
1. Check for deprecated TypoScript objects and properties
2. Identify CONTENT objects that should migrate to DataProcessors
3. Look for deprecated stdWrap properties
4. Check conditions syntax (old vs Symfony expression language)
5. Review plugin configurations for deprecated settings
6. Check for file references using deprecated paths
Provide migration guidance for each issue.
severity: warning
desc: "TypoScript modernization review"
# === FLUID TEMPLATE UPGRADE ===
- id: TU-47
domain: templates
prompt: |
Review Fluid templates for upgrade compatibility:
1. Check for deprecated ViewHelpers (f:widget.*, f:be.*)
2. Identify custom ViewHelpers that might need updates
3. Look for deprecated layout/partial references
4. Check for inline JavaScript that should be modernized
5. Verify namespace declarations are current
6. Check for deprecated format ViewHelpers
List each template needing updates with specific changes required.
severity: warning
desc: "Fluid template upgrade compatibility"
# === BACKEND MODULE MIGRATION ===
- id: TU-48
domain: backend
prompt: |
Check backend module configuration:
1. If ext_tables.php contains module registration, recommend Configuration/Backend/Modules.php
2. Check backend controller classes for deprecated parent classes
3. Verify backend module templates use modern structure
4. Check for deprecated JavaScript module loading
5. Review backend route configurations
6. Check for deprecated backend user permission checks
Provide migration path for each issue.
severity: warning
desc: "Backend module migration readiness"
# === DEPENDENCY INJECTION READINESS ===
- id: TU-49
domain: architecture
prompt: |
Evaluate dependency injection implementation:
1. Check if Services.yaml properly configures all service classes
2. Look for makeInstance calls that should use DI
3. Identify singleton patterns that could use DI
4. Check for @inject annotations that should be constructor injection
5. Verify interfaces are properly configured for DI
6. Check for ObjectManager::getInstance usage
Rate DI adoption and list classes needing refactoring.
severity: warning
desc: "Dependency injection adoption assessment"
# === UPGRADE PATH SUMMARY ===
- id: TU-50
domain: upgrade-planning
prompt: |
Provide an overall upgrade readiness assessment:
1. Summarize all blocking issues that must be fixed before upgrade
2. List deprecated patterns that should be addressed
3. Estimate effort level (low/medium/high) for the upgrade
4. Recommend upgrade sequence if multiple TYPO3 versions are being skipped
5. Identify any third-party dependencies that might block upgrade
6. Suggest testing strategy for the upgrade
Create a prioritized action list for the upgrade process.
severity: info
desc: "Overall upgrade readiness and planning summary"
# === THIRD-PARTY DEPENDENCY API COMPATIBILITY ===
- id: TU-51
domain: dependency-compatibility
prompt: |
Analyze composer.json for third-party dependencies (non-TYPO3, non-PHP) that use
multi-version constraints (e.g., "^3.0 || ^4.0"):
1. For each such dependency, enumerate ALL usages of its API in Classes/ and Tests/
2. Cross-reference method calls against the dependency's interface definitions
3. Flag any method called on an interface-typed variable that only exists on the
concrete class or was removed/renamed in a newer major version
4. Check for @phpstan-ignore tags that mask version-conditional method calls —
these indicate a runtime error in one of the supported versions
5. Verify adapter pattern is used where method signatures differ between versions
Report each incompatibility with file:line reference and recommended fix.
severity: error
desc: "Third-party dependency API compatibility across all supported major versions"
# === PHPSTAN MULTI-VERSION VALIDATION ===
- id: TU-52
domain: static-analysis
prompt: |
Check PHPStan configuration and CI setup for multi-version dependency support:
1. If composer.json has any dependency with "||" constraints spanning major versions,
verify that PHPStan is configured to run against EACH major version
2. Check CI workflow (GitHub Actions / GitLab CI) for a test matrix that includes
each supported major version of multi-version dependencies
3. Look for @phpstan-ignore or @phpstan-ignore-next-line annotations in Classes/ —
each one should be reviewed to ensure it doesn't mask a real runtime error
in one of the supported versions
4. Check if method_exists() is used for version detection — if called on an instance
variable, PHPStan type narrowing can cause issues; recommend checking on the
class/interface name instead, or using the adapter pattern
5. Verify phpstan.neon does not pin a specific dependency version that would skip
validation of other supported versions
Report PHPStan gaps and recommend CI matrix additions.
severity: warning
desc: "PHPStan must validate against each supported major version of multi-version dependencies"
# === TEST MOCK COMPATIBILITY ===
- id: TU-53
domain: testing
prompt: |
Review test files for compatibility with multi-version dependencies:
1. Find all createMock() or getMockBuilder() calls that mock third-party interfaces
2. For each mock, verify that every ->method('name') call references a method
that exists on the interface in ALL supported major versions
3. Check willReturnCallback() closures — their parameter signatures must match
the mocked method's signature in each supported version
4. Look for test specificity loss: if production code was refactored from a
version-specific API (e.g., ->toWebp()->save()) to a version-agnostic API
(e.g., ->save()), verify that test assertions remain equally specific
(e.g., asserting output format, file extension, or quality parameters)
5. Check that test suites are configured to run against each supported dependency
version in CI
Report each incompatible mock with file:line and recommended fix.
severity: error
desc: "Test mocks must reference methods valid on interfaces in all supported dependency versions"
# === SINGLETONINTERFACE DEPRECATION (TYPO3 v14) ===
- id: TU-55
domain: code-quality
prompt: |
Review the codebase for uses of TYPO3\CMS\Core\SingletonInterface.
Check for:
1. Classes implementing SingletonInterface — this is deprecated in TYPO3 v14
2. Suggest migration to proper DI container scoping (shared: true in Services.yaml)
3. EXCEPTION: Classes that must be used with GeneralUtility::setSingletonInstance()
in test fixtures. These may need to keep the interface temporarily until
the test setup is refactored to use proper DI container overrides.
4. Check if the singleton behavior is actually needed or if the class can
simply be a regular shared service (which is the DI container default).
Report specific files implementing SingletonInterface and migration path.
severity: warning
desc: "SingletonInterface is deprecated in TYPO3 v14 — migrate to DI container scoping"
API Changes Reference
Search patterns, replacements, and key breaking changes organized by TYPO3 version upgrade path.
---
v7 → v8 Upgrade
Database Layer: TYPO3_DB → Doctrine DBAL
Search Pattern
grep -rn "\$GLOBALS\['TYPO3_DB'\]\|exec_SELECTquery\|exec_INSERTquery\|exec_UPDATEquery\|exec_DELETEquery" Classes/Replace
| Before (v7) | After (v8+) |
|---|---|
$GLOBALS['TYPO3_DB']->exec_SELECTquery() | Use QueryBuilder |
$GLOBALS['TYPO3_DB']->exec_INSERTquery() | $queryBuilder->insert() |
$GLOBALS['TYPO3_DB']->exec_UPDATEquery() | $queryBuilder->update() |
$GLOBALS['TYPO3_DB']->exec_DELETEquery() | $queryBuilder->delete() |
$GLOBALS['TYPO3_DB']->fullQuoteStr() | $queryBuilder->createNamedParameter() |
Example Migration
// Before (v7)
$rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'tt_content', 'pid=' . $pid);
// After (v8+)
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
->select('*')
->from('tt_content')
->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)))
->executeQuery()
->fetchAllAssociative();ExtJS/Prototype Removal
Search Pattern
grep -rn "Ext\.onReady\|new Ext\.\|Prototype\.\|Element\.extend" Resources/Fix: Replace with vanilla JavaScript or jQuery.
Icon Factory
Search Pattern
grep -rn "IconUtility::\|t3skin\|t3lib_iconWorks" Classes/Replace
| Before | After |
|---|---|
IconUtility::getSpriteIcon() | IconFactory->getIcon() |
IconUtility::getSpriteIconForRecord() | IconFactory->getIconForRecord() |
---
v8 → v9 Upgrade
Site Configuration Introduction
Search Pattern
grep -rn "sys_domain\|config\.baseURL\|absRefPrefix\s*=\s*auto" Configuration/Fix: Migrate to Site Configuration (config/sites/*/config.yaml).
PSR-15 Middleware
Search Pattern
grep -rn "AbstractUserAuthentication\|tslib_fe\|tslib_cObj" Classes/Replace
| Before | After |
|---|---|
tslib_fe | TypoScriptFrontendController |
tslib_cObj | ContentObjectRenderer |
| Hook-based request handling | PSR-15 middleware |
Routing API
Search Pattern
grep -rn "tx_realurl\|cooluri\|simulatestatic" Configuration/Fix: Migrate to native TYPO3 Routing with Site Configuration.
Signal/Slot Deprecation Start
Search Pattern
grep -rn "SignalSlotDispatcher\|->connect\(" Classes/Note: Mark for migration to PSR-14 Events (complete in v10).
---
v9 → v10 Upgrade
Symfony 5 Upgrade
Search Pattern
grep -rn "Symfony\\\\Component\\\\Console\\\\Command\|setDescription\|setHelp" Classes/Command/Replace: Update command registration to use Services.yaml.
Dependency Injection
Search Pattern
grep -rn "GeneralUtility::makeInstance\|ObjectManager::get" Classes/Replace: Use constructor injection with Services.yaml.
// Before
$service = GeneralUtility::makeInstance(MyService::class);
// After (Services.yaml)
services:
Vendor\Extension\Service\MyService:
public: truePSR-14 Events
Search Pattern
grep -rn "SignalSlotDispatcher\|->emit\|->connect\(" Classes/Replace
| Before | After |
|---|---|
Signal/Slot connect() | Event Listener via Services.yaml |
Signal/Slot dispatch() | EventDispatcher->dispatch(new Event()) |
Fluid Namespace
Search Pattern
grep -rn "{namespace\|xmlns:f=" Resources/Private/Replace: Use XML namespace declarations.
<!-- Before -->
{namespace v=Vendor\Extension\ViewHelpers}
<!-- After -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:v="http://typo3.org/ns/Vendor/Extension/ViewHelpers"
data-namespace-typo3-fluid="true">---
v10 → v11 Upgrade
Fluid Standalone
Search Pattern
grep -rn "TYPO3Fluid\|StandaloneView\|setTemplatePathAndFilename" Classes/Replace
// Before
$view->setTemplatePathAndFilename($templatePath);
// After
$view->setTemplate($templatePath);
$view->setTemplateRootPaths([$rootPath]);Backend Controller Changes
Search Pattern
grep -rn "extends ActionController\|AbstractModule" Classes/Controller/Note: Backend modules must return ResponseInterface.
// Before
public function indexAction() {
$this->view->assign('data', $data);
}
// After
public function indexAction(): ResponseInterface {
$this->view->assign('data', $data);
return $this->htmlResponse();
}TCA Wizard Changes
Search Pattern
grep -rn "'wizards'\s*=>\|'wizard_'" Configuration/TCA/Replace: Use fieldControl, fieldInformation, fieldWizard.
---
v11 → v12 Upgrade
Doctrine DBAL 4.x (Critical)
PDO Constants Removed
Search Pattern
grep -rn "PDO::PARAM_" Classes/Replace
| Before | After |
|---|---|
PDO::PARAM_INT | Connection::PARAM_INT |
PDO::PARAM_STR | Connection::PARAM_STR |
PDO::PARAM_BOOL | Connection::PARAM_BOOL |
PDO::PARAM_NULL | Connection::PARAM_NULL |
Required Import
use TYPO3\CMS\Core\Database\Connection;QueryBuilder execute() Removed
Search Pattern
grep -rn "->execute()" Classes/Replace
| Before (DBAL 3.x) | After (DBAL 4.x) |
|---|---|
$queryBuilder->execute() (SELECT) | $queryBuilder->executeQuery() |
$queryBuilder->execute() (INSERT/UPDATE/DELETE) | $queryBuilder->executeStatement() |
Example Migration
// Before (DBAL 3.x)
$result = $queryBuilder
->select('*')
->from('pages')
->execute();
// After (DBAL 4.x)
$result = $queryBuilder
->select('*')
->from('pages')
->executeQuery();ParameterType Enum (PHPStan Warning)
In Doctrine DBAL 4.x, type parameters changed from int to ParameterType enum:
// This works but PHPStan may complain:
->createNamedParameter($value, Connection::PARAM_INT)
// PHPStan ignore pattern for dual v12/v13 compatibility:
# Build/phpstan.neon
parameters:
ignoreErrors:
- '~Parameter \\#2 \\$type of method .* expects .*, int given~'Note: TYPO3 Core provides Connection::PARAM_* constants that work across versions, but PHPStan may still report type mismatches during the transition period
GeneralUtility Deprecated Methods
Search Pattern
grep -rn "GeneralUtility::_GET\|GeneralUtility::_POST\|GeneralUtility::_GP" Classes/Replace
| Before | After |
|---|---|
GeneralUtility::_GET('param') | $_GET['param'] ?? null |
GeneralUtility::_POST('param') | $_POST['param'] ?? null |
GeneralUtility::_GP('param') | $_GET['param'] ?? $_POST['param'] ?? null |
For Controllers/Middleware (PSR-7)
// GET parameters
$value = $request->getQueryParams()['param'] ?? null;
// POST parameters
$value = $request->getParsedBody()['param'] ?? null;TCA Required Field
Search Pattern
grep -rn "'eval'.*'required'" Configuration/TCA/Replace
// Before
'config' => [
'type' => 'input',
'eval' => 'required,trim',
],
// After
'config' => [
'type' => 'input',
'required' => true,
'eval' => 'trim',
],TCA inputLink → type=link
Search Pattern
grep -rn "'renderType' => 'inputLink'" Configuration/TCA/Replace
// Before (deprecated in v12)
'config' => [
'type' => 'input',
'renderType' => 'inputLink',
'eval' => 'trim',
],
// After (v12+)
'config' => [
'type' => 'link',
],Note: The type=link field automatically handles link browsing, no renderType needed.
Form Element Data Structure
Search Pattern
grep -rn "itemFormElID" Classes/Fix Pattern
// Before (removed in v12)
$id = $this->data['parameterArray']['itemFormElID'];
// After
$baseId = str_replace(['[', ']'], '_', $this->data['parameterArray']['itemFormElName']);
$baseId = trim($baseId, '_');xml2array Null Handling
Search Pattern
grep -rn "xml2array" Classes/Fix Pattern
// Before
if ($row['field'] !== '') {
$config = GeneralUtility::xml2array($row['field']);
}
// After
if (!empty($row['field'])) {
$config = (array) GeneralUtility::xml2array((string) $row['field']);
}FlexForm Structure (Fractor handles)
Search Pattern
grep -rn "<required>1</required>" Configuration/FlexForms/Fix: Run Fractor - migrates to <required>true</required>.
TypoScript Conditions
Search Pattern
grep -rn "\[end\]" Configuration/TypoScript/Replace: [end] → [global] (Fractor handles).
Click Menu Parameters
Search Pattern
grep -rn "BackendUtility::wrapClickMenuOnIcon\|getClickMenuOnIconTagParameters" Classes/Fix: Remove 4th parameter if 'true' or '1'.
---
v12 → v13 Upgrade
Request Attributes (Critical)
Search Pattern
grep -rn "\$TSFE->fe_user\|\$GLOBALS\['TSFE'\]->fe_user" Classes/Replace
| Before (v12) | After (v13) |
|---|---|
$TSFE->fe_user | $request->getAttribute('frontend.user') |
$TSFE->page | $request->getAttribute('frontend.page.information')->getPageRecord() |
$TSFE->rootLine | $request->getAttribute('frontend.page.information')->getRootLine() |
$TSFE->id | $request->getAttribute('frontend.page.information')->getId() |
Site Sets Introduction
Search Pattern
grep -rn "ext_typoscript_setup\.typoscript\|ext_typoscript_constants"Replace: Migrate to Site Sets (Configuration/Sets/).
# Configuration/Sets/MySet/config.yaml
name: vendor/my-set
label: My Extension Set
dependencies:
- typo3/fluid-styled-contentBackend Module Registration
Search Pattern
grep -rn "registerModule\|TYPO3_MOD_PATH" ext_tables.phpReplace: Use Configuration/Backend/Modules.php.
TCA Type Changes
Search Pattern
grep -rn "'type'\s*=>\s*'text'" Configuration/TCA/Note: Review type => text fields for migration to type => json where applicable.
---
v13 → v14 Upgrade
TypoScript/TSconfig Callables Require #[AsAllowedCallable] Attribute (Critical)
Breaking: #108054 - Changelog
TYPO3 v14 requires explicit opt-in for methods callable through TypoScript/TSconfig. Without the attribute, calls fail with AllowedCallableException.
Search Pattern
# Find TypoScript userFunc references
grep -rn "userFunc\|preUserFunc\|postUserFunc" Configuration/TypoScript/
# Find PHP classes referenced in TypoScript
grep -rn "->render\|->process" Configuration/TypoScript/ | grep -v "#"Affected
userFuncin USER/USER_INT content objectspreUserFunc,postUserFunc,preUserFuncInt,postUserFuncIntin stdWrap- TSconfig
renderFuncin suggest wizard
Fix: Add #[AsAllowedCallable] attribute to all TypoScript-callable methods:
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
class MyProcessor
{
#[AsAllowedCallable]
public function process(?string $content, array $conf, ServerRequestInterface $request): string
{
return $content ?? '';
}
}Version Compatibility: The attribute was backported to TYPO3 13.4.21. Extensions requiring ^13.4 || ^14.0 must update minimum to ^13.4.21 || ^14.0.
Note: No Rector rule exists for this change. Manual review of TypoScript configurations required.
ExtensionConfiguration::getAll() Not Available (Critical)
Search Pattern
grep -rn "ExtensionConfiguration::getAll\|->getAll()" Classes/Replace — getAll() is not available; inject ExtensionConfiguration and read each extension's configuration with ->get() rather than touching $GLOBALS directly.
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
class MyService
{
public function __construct(
private readonly ExtensionConfiguration $extensionConfiguration,
) {}
public function readConfig(): void
{
// Before (no longer available)
// $allConfig = GeneralUtility::makeInstance(ExtensionConfiguration::class)->getAll();
// After - fetch per extension
$extConfig = $this->extensionConfiguration->get('my_extension');
// A specific key/path within the extension's configuration
$value = $this->extensionConfiguration->get('my_extension', 'features/enableFoo');
}
}Doctrine DBAL 4.x Type::getName() Removed
Search Pattern
grep -rn "->getName()\|Type::getName" Classes/Replace
// Before (DBAL 3.x)
use Doctrine\DBAL\Types\Type;
$typeName = $column->getType()->getName();
if ($typeName === 'string' || $typeName === 'text') { }
// After (DBAL 4.x) - use instanceof checks
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\BlobType;
$type = $column->getType();
if ($type instanceof StringType || $type instanceof TextType) { }Icon::SIZE_* Constants Replaced with IconSize Enum
Search Pattern
grep -rn "Icon::SIZE_SMALL\|Icon::SIZE_DEFAULT\|Icon::SIZE_MEDIUM\|Icon::SIZE_LARGE" Classes/Replace
// Before (deprecated)
use TYPO3\CMS\Core\Imaging\Icon;
$icon = $iconFactory->getIcon('actions-edit', Icon::SIZE_SMALL);
// After (v14+)
use TYPO3\CMS\Core\Imaging\IconSize;
$icon = $iconFactory->getIcon('actions-edit', IconSize::SMALL);| Before | After |
|---|---|
Icon::SIZE_SMALL | IconSize::SMALL |
Icon::SIZE_DEFAULT | IconSize::DEFAULT |
Icon::SIZE_MEDIUM | IconSize::MEDIUM |
Icon::SIZE_LARGE | IconSize::LARGE |
Icon::SIZE_MEGA | IconSize::MEGA |
Icon::SIZE_OVERLAY | IconSize::OVERLAY |
f:uri.resource Not Available in Non-Extbase Modules
Search Pattern
grep -rn "f:uri.resource\|<f:uri.resource" Resources/Private/Replace
<!-- Before (fails in non-Extbase backend modules) -->
<link rel="stylesheet" href="{f:uri.resource(path:'Css/backend.css')}" />
<!-- After - use EXT: syntax directly -->
<link rel="stylesheet" href="EXT:my_extension/Resources/Public/Css/backend.css" />Reason: In TYPO3 v14, f:uri.resource requires an Extbase Request object which is not available in non-Extbase backend modules. Using EXT: syntax works in all contexts.
Scheduler Interface Signature Changes
Search Pattern
grep -rn "AdditionalFieldProviderInterface\|getAdditionalFields" Classes/Task/Fix: Match exact interface signature - remove type hints that don't match:
// Before (may have extra type hints)
public function getAdditionalFields(
array &$taskInfo,
AbstractTask $task, // ❌ Type hint not in interface
SchedulerModuleController $parentObject
): array
// After - match interface exactly
public function getAdditionalFields(
array &$taskInfo,
$task, // ✅ No type hint (matches interface)
SchedulerModuleController $parentObject
): arrayBootstrap 5 CSS Class Changes
TYPO3 v14 uses Bootstrap 5. Update CSS classes in templates.
Search Pattern
grep -rn "btn-default\|badge-primary\|badge-success\|badge-danger\|badge-warning\|badge-info" Resources/Replace
| Before (Bootstrap 4) | After (Bootstrap 5) |
|---|---|
btn-default | btn-secondary |
badge-primary | text-bg-primary |
badge-success | text-bg-success |
badge-danger | text-bg-danger |
badge-warning | text-bg-warning |
badge-info | text-bg-info |
TCA renderType vs type
When checking for custom TCA field types, use renderType, not type.
Search Pattern
grep -rn "\['config'\]\['type'\]" Classes/Hook/Fix
// ❌ Wrong - checks base type (input, text, etc.)
$renderType = $fieldConfig['config']['type'] ?? '';
// ✅ Correct - checks custom render type
$renderType = $fieldConfig['config']['renderType'] ?? '';ARIA Accessibility Requirements
TYPO3 v14 enforces stricter WCAG 2.1 AA compliance. Add ARIA attributes to templates.
Search Pattern
grep -rn "role=\"main\"\|aria-label\|aria-describedby" Resources/Private/Required Additions
<!-- Main content wrapper -->
<div class="module" role="main" aria-label="My Module">
<!-- Tables need captions -->
<table>
<caption class="visually-hidden">List of items</caption>
<thead>
<tr>
<th scope="col">Name</th>
</tr>
</thead>
</table>
<!-- Interactive elements need labels -->
<button aria-label="Delete item">
<core:icon identifier="actions-delete" />
</button>Dual v13/v14 Compatibility Pattern
For extensions supporting both versions:
// IconSize enum compatibility
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconSize;
// Check if IconSize enum exists (v14+)
if (class_exists(IconSize::class)) {
$size = IconSize::SMALL;
} else {
$size = Icon::SIZE_SMALL;
}
// Or use string (works in both)
$icon = $iconFactory->getIcon('actions-edit', 'small');GeneralUtility::getIndpEnv() Deprecated -- Use NormalizedParams (v14.3)
Deprecated: v14.3, scheduled for removal in v15.0
Source:typo3/cms-corev14.3.0 vendor source,Classes/Utility/GeneralUtility.php(the@deprecatedtag is on the method itself).
Verify locally:grep -n -B5 "function getIndpEnv(" vendor/typo3/cms-core/Classes/Utility/GeneralUtility.php-- shows the docblock with@deprecated since TYPO3 v14.3, will be removed in TYPO3 v15.0directly above the method signature (line ~2142 in v14.3.0).
GeneralUtility::getIndpEnv($name) is deprecated since TYPO3 v14.3 and scheduled for removal in v15.0. Replacement is NormalizedParams from the PSR-7 request.
⚠️ Don't trust AI assistants on this deprecation timing. Gemini Code Assist has been observed claiminggetIndpEnv()was deprecated in v13.0 / removed in v14.0, and thatNormalizedParams::createFromServerParams()was removed in v14.0 in favour of a (non-existent)NormalizedParamsFactoryclass. All three claims are wrong -- verified false againsttypo3/cms-corev14.3.0 vendor source. Always verify deprecation timing by reading the@deprecatedannotation invendor/typo3/cms-core/.
Search Pattern
grep -rn "GeneralUtility::getIndpEnv\|::getIndpEnv(" Classes/ Configuration/Method mapping (getIndpEnv($name) → NormalizedParams method)
getIndpEnv() argument | NormalizedParams method |
|---|---|
'REMOTE_ADDR' | getRemoteAddress() |
'HTTP_HOST' | getHttpHost() |
'TYPO3_SSL' | isHttps() |
'HTTP_REFERER' | getHttpReferer() |
'REQUEST_URI' | getRequestUri() |
'SCRIPT_NAME' | getScriptName() |
'TYPO3_REQUEST_HOST' | getRequestHost() |
'TYPO3_REQUEST_URL' | getRequestUrl() |
'TYPO3_SITE_URL' | getSiteUrl() |
'TYPO3_SITE_PATH' | getSitePath() |
Replace -- in controllers/middleware (request injected)
// ❌ Before
$ip = GeneralUtility::getIndpEnv('REMOTE_ADDR');
// ✅ After (PSR-7 request available)
$ip = $request->getAttribute('normalizedParams')?->getRemoteAddress() ?? '';Replace -- in services/auth services (no request injected)
For services where ServerRequestInterface cannot be injected (e.g. AbstractAuthenticationService subclasses, CLI commands), fall back through $GLOBALS['TYPO3_REQUEST'] and finally re-create NormalizedParams from $_SERVER:
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\NormalizedParams;
private function getRemoteAddress(): string
{
// 1. Prefer PSR-7 request (set in middleware-driven flows)
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request instanceof ServerRequestInterface) {
$params = $request->getAttribute('normalizedParams');
if ($params instanceof NormalizedParams) {
return $params->getRemoteAddress();
}
}
// 2. CLI / test fallback -- createFromServerParams() exists and is callable
// in v14.3 (verified at NormalizedParams.php:306, marked only @internal,
// NOT @deprecated).
$confVars = $GLOBALS['TYPO3_CONF_VARS'] ?? null;
$sysConf = is_array($confVars) && isset($confVars['SYS']) && is_array($confVars['SYS'])
? $confVars['SYS']
: [];
return NormalizedParams::createFromServerParams($_SERVER, $sysConf)->getRemoteAddress();
}Dual-version compatibility (v12.4 + v13.4 + v14.3)
The migration is safe across the entire supported range with no compatibility shims: NormalizedParams has been part of TYPO3 since v9.4, and normalizedParams has been a request attribute since v10. Just migrate to NormalizedParams in one step -- it works on v12.4 / v13.4 / v14.3 unchanged, and silences the v14.3 deprecation.
Reference migration: netresearch/t3x-nr-passkeys-be commit b2cfd8e (v14.3 upgrade #57).
No Rector rule yet (as of v14.3 / typo3-rector 3.x). Manual grep + replace required.
Additional v14 Changes
Monitor Changelog-14 for additional breaking changes.
Known Removals
ExtensionConfiguration::getAll()- use$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']Type::getName()in Doctrine DBAL - useinstanceofchecksIcon::SIZE_*constants - useIconSizeenum
Known Deprecations (v14.3, removal in v15.0)
GeneralUtility::getIndpEnv()- useNormalizedParamsfrom PSR-7 request
---
PHP 8.4 Compatibility
These changes are required for PHP 8.4 compatibility regardless of TYPO3 version.
Implicit Nullable Parameters (Critical)
PHP 8.4 deprecates implicit nullable parameters. This affects all TYPO3 versions.
Search Pattern
# Find parameters with null default but no explicit nullable type
grep -rn '\(.*\$[a-zA-Z_]* = null\)' Classes/ | grep -v '?[a-zA-Z_\\]*\s*\$'Replace
// ❌ Deprecated in PHP 8.4 (E_DEPRECATED), Error in PHP 9.0
public function foo(string $param = null): void
public function bar(array $config = null): void
// ✅ Required - Explicit nullable type
public function foo(?string $param = null): void
public function bar(?array $config = null): voidCommon Occurrences
- FlexForm configuration parameters
- Optional service dependencies in constructors
- Default TCA configuration arrays
- Callback/hook method signatures
Rector Rule: Use NullableTypeDeclarationRector to auto-fix.
TCA Items Array Format
Search Pattern
grep -rn "'items'\s*=>\s*\[" Configuration/TCA/ | grep -v "label"Replace
// ❌ Old format (deprecated)
'items' => [
['Label', 'value'],
['Other Label', 'other_value'],
],
// ✅ New format (TYPO3 v12+)
'items' => [
['label' => 'Label', 'value' => 'value'],
['label' => 'Other Label', 'value' => 'other_value'],
],---
PSR-7 Request Handling Patterns
Query Parameter Access in Context Classes
When migrating context matching or middleware that needs request data:
Search Pattern
grep -rn "\$_GET\['\|\$_POST\['" Classes/Replace Pattern
// ❌ Old pattern - doesn't work with PSR-7 request flow
$value = $_GET['param'] ?? null;
// ✅ TYPO3 v12+ - Use PSR-7 request from container/middleware
use Psr\Http\Message\ServerRequestInterface;
class MyContext
{
private ?ServerRequestInterface $request = null;
public function setRequest(ServerRequestInterface $request): self
{
$this->request = $request;
return $this;
}
public function match(): bool
{
// Get from stored request first, fallback to GLOBALS
$request = $this->request
?? $GLOBALS['TYPO3_REQUEST']
?? null;
if ($request === null) {
return false;
}
$value = $request->getQueryParams()['param'] ?? null;
return $value === 'expected';
}
}Middleware Request Passing
// In middleware, pass request to context/service
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Store request for context matching
Container::get()->setRequest($request)->initMatching();
return $handler->handle($request);
}---
SC_OPTIONS Hooks to PSR-14 Events
Page Visibility Hooks (Critical for v12+)
SC_OPTIONS hooks for page visibility may not work correctly in TYPO3 v12+.
Search Pattern
grep -rn "SC_OPTIONS\['t3lib/class.t3lib_page.php'\]" ext_localconf.php
grep -rn "additionalQueryRestrictions" ext_localconf.phpIssue: Query restrictions registered in additionalQueryRestrictions may not be applied during page resolution in v12+.
Solution: Migrate to PSR-14 events:
// Configuration/Services.yaml
services:
Vendor\Extension\EventListener\PageAccessListener:
tags:
- name: event.listener
identifier: 'vendor-extension/page-access'
event: TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent// Classes/EventListener/PageAccessListener.php
use TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent;
final class PageAccessListener
{
public function __invoke(BeforePageIsRetrievedEvent $event): void
{
// Check context and modify page access
$pageId = $event->getPageId();
// ... context matching logic
}
}Note: Test page visibility thoroughly after migration. Some hook-based restrictions require complete PSR-14 event migration to work correctly.
---
Dual Version Compatibility Matrix
When supporting multiple versions (e.g., ^12.4 || ^13.4):
| API | v11 | v12 | v13 | v14 |
|---|---|---|---|---|
$GLOBALS['TYPO3_DB'] | ❌ | ❌ | ❌ | ❌ |
QueryBuilder | ✅ | ✅ | ✅ | ✅ |
PDO::PARAM_* | ✅ | ❌ | ❌ | ❌ |
Connection::PARAM_* | ✅ | ✅ | ✅ | ✅ |
GeneralUtility::_GET() | ✅ | ⚠️ | ❌ | ❌ |
$_GET['param'] ?? null | ✅ | ✅ | ✅ | ✅ |
$TSFE->fe_user | ✅ | ✅ | ⚠️ | ❌ |
$request->getAttribute('frontend.user') | ❌ | ❌ | ✅ | ✅ |
GeneralUtility::getIndpEnv() | ✅ | ✅ | ✅ | ⚠️ (v14.3) |
$request->getAttribute('normalizedParams') | ✅ | ✅ | ✅ | ✅ |
| Signal/Slot | ⚠️ | ❌ | ❌ | ❌ |
| PSR-14 Events | ✅ | ✅ | ✅ | ✅ |
Legend: ✅ Supported | ⚠️ Deprecated | ❌ Removed
Rule: For dual compatibility, always use the older-version-compatible API.
---
Quick Search Commands
# v7→v8: Old database API
grep -rn "TYPO3_DB\|exec_SELECTquery" Classes/
# v8→v9: Old routing
grep -rn "tx_realurl\|cooluri\|sys_domain"
# v9→v10: Signal/Slot
grep -rn "SignalSlotDispatcher\|->connect\(" Classes/
# v10→v11: Old Fluid
grep -rn "setTemplatePathAndFilename" Classes/
# v11→v12: PDO constants & deprecated methods
grep -rn "PDO::PARAM_\|GeneralUtility::_GET\|itemFormElID" Classes/
# v12→v13: TSFE direct access
grep -rn "\$TSFE->fe_user\|\$TSFE->page\|\$TSFE->rootLine" Classes/
# v13→v14.3: getIndpEnv() deprecation (use NormalizedParams)
grep -rn "GeneralUtility::getIndpEnv\|::getIndpEnv(" Classes/ Configuration/
# PHP 8.4: Implicit nullable parameters
grep -rn '\$[a-zA-Z_]* = null)' Classes/ | grep -v '?'
# PHP 8.4: Old TCA items format
grep -rn "'items'\s*=>" Configuration/TCA/ | head -20
# PSR-7: Direct superglobal access
grep -rn "\$_GET\['\|\$_POST\['" Classes/
# SC_OPTIONS hooks (may need PSR-14 migration)
grep -rn "SC_OPTIONS" ext_localconf.php
# All versions: Full deprecation scan
grep -rn "@deprecated\|trigger_error.*E_USER_DEPRECATED" Classes/---
Verification Commands
After making changes:
# Static analysis
./vendor/bin/phpstan analyse
# Code style
./vendor/bin/php-cs-fixer fix --dry-run --diff
# Unit tests
./vendor/bin/phpunit --testsuite Unit
# Functional tests
./vendor/bin/phpunit --testsuite Functional---
Testing Infrastructure During Upgrades
When upgrading extensions, tests often need adjustments to work with new TYPO3 versions.
Functional Test Container Isolation
In TYPO3 v12+, singleton services may persist state between tests. Reset the Container before each test:
Problem Pattern
// Tests pass individually but fail when run together
// due to state pollution from singleton servicesFix Pattern
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Core\Bootstrap;
protected function setUp(): void
{
parent::setUp();
// Reset Container to ensure clean state
GeneralUtility::resetSingletonInstances([]);
// Flush all caches for complete reset
Bootstrap::initializeBackendRouter();
}Session State in Test Fixtures
When testing features that use session storage (e.g., context matching), disable sessions in fixtures:
CSV Fixture Pattern
# tx_myext_records.csv
# Set use_session=0 to prevent session state from persisting between tests
"tx_myext_records"
,"uid","pid","title","use_session","disabled"
,1,1,"Test Record",0,0Why: Session-based features can cause flaky tests when session state persists between test runs.
PHPUnit Configuration for Dual Version Support
When supporting ^12.4 || ^13.4, PHPUnit configuration may need adjustments:
Coverage Driver Consideration
<!-- Build/phpunit/UnitTests.xml -->
<phpunit>
<coverage>
<!-- Requires xdebug or pcov extension -->
<include>
<directory suffix=".php">../../Classes</directory>
</include>
</coverage>
</phpunit>CI without Coverage Driver
# If no coverage driver installed, use --no-coverage flag
./vendor/bin/phpunit --no-coverageTesting Framework Version Matrix
| TYPO3 Version | Testing Framework | PHPUnit |
|---|---|---|
| v11.5 | ^7.0 | ^9.0 |
| v12.4 | ^8.0 | `^10.0 \ |
| v13.4 | ^9.0 | `^11.0 \ |
Database Credentials in DDEV
Functional tests in DDEV require database credentials:
# Auto-detect DDEV and set credentials
if command -v ddev &> /dev/null && ddev describe &> /dev/null; then
export typo3DatabaseDriver=mysqli
export typo3DatabaseHost=db
export typo3DatabasePort=3306
export typo3DatabaseName=db
export typo3DatabaseUsername=db
export typo3DatabasePassword=db
fiE2E Test Compatibility
Playwright E2E tests may need adjustments for TYPO3 version differences:
Locator Changes
// TYPO3 v12 vs v13 may have different CSS selectors
// Use data-testid attributes for stability
await page.locator('[data-testid="login-submit"]').click();Timeout Adjustments
// TYPO3 v13 backend may take longer to initialize
test.setTimeout(60000); // 60 seconds for complex backend operations---
Tooling Configuration Changes
PHPStan Deprecation Handling
Doctrine DBAL 4.x type changes may cause PHPStan warnings even with correct code:
# Build/phpstan.neon
parameters:
ignoreErrors:
# Doctrine DBAL 4.x ParameterType enum compatibility
- '~Parameter \\#2 \\$type of method .* expects .*, int given~'GrumPHP with PHPStan
Prevent GrumPHP failures on non-PHP commits:
# grumphp.yml
grumphp:
tasks:
phpstan:
# Only trigger on PHP file changes
triggered_by: ['php']Build Directory Organization
Modern TYPO3 extensions organize tooling in Build/:
Build/
├── phpstan.neon # PHPStan configuration
├── phpunit/
│ ├── UnitTests.xml
│ └── FunctionalTests.xml
├── Scripts/
│ └── runTests.sh # Test runner script
└── rector.php # Rector configuration (optional)CI Workflow Path Updates
When moving configs to Build/, update CI workflows:
# .github/workflows/ci.yml
- name: Run PHPStan
run: vendor/bin/phpstan analyse -c Build/phpstan.neon
- name: Run Tests
run: vendor/bin/phpunit -c Build/phpunit/UnitTests.xml---
Symfony Component Deprecations
TYPO3 extensions often use Symfony components directly. Be aware of these deprecations.
PropertyInfo Type Class (Symfony 7.3+)
Source: netresearch/sdk-api-universal-messenger CI fix (2024-12)
The Symfony\Component\PropertyInfo\Type class and its constants are deprecated since Symfony 7.3.
Search Pattern
grep -rn "PropertyInfo\\Type\|Type::BUILTIN_TYPE_" Classes/Replace
| Before (Deprecated) | After |
|---|---|
Type::BUILTIN_TYPE_BOOL | 'bool' |
Type::BUILTIN_TYPE_INT | 'int' |
Type::BUILTIN_TYPE_FLOAT | 'float' |
Type::BUILTIN_TYPE_STRING | 'string' |
Type::BUILTIN_TYPE_ARRAY | 'array' |
Type::BUILTIN_TYPE_OBJECT | 'object' |
Type::BUILTIN_TYPE_NULL | 'null' |
Type::BUILTIN_TYPE_CALLABLE | 'callable' |
Type::BUILTIN_TYPE_ITERABLE | 'iterable' |
Example Migration
// Before (deprecated since Symfony 7.3)
use Symfony\Component\PropertyInfo\Type;
$encoder->addType(
Type::BUILTIN_TYPE_BOOL,
function ($name, $value) { /* ... */ }
);
// After - use string literal directly
$encoder->addType(
'bool',
function ($name, $value) { /* ... */ }
);PHPStan Detection: Enable phpstan/phpstan-deprecation-rules to catch this automatically.
---
Composer Dependency Version Constraints
PHP Version Constraints
Source: netresearch/sdk-api-universal-messenger CI fix (2024-12)
Always specify minimum PHP version, not just maximum:
{
"require": {
"php": "^8.2"
}
}Common Mistakes
// ❌ BAD: No minimum specified - breaks on older PHP
{
"require": {
"php": "<8.6.0"
}
}
// ✅ GOOD: Clear version range
{
"require": {
"php": "^8.2"
}
}Dependency PHP Version Conflicts
When supporting PHP 8.2, watch for dependencies that require newer PHP versions:
| Package | Version | Requires PHP |
|---|---|---|
symfony/http-client | ^8.0 | PHP ≥8.4 |
symfony/http-client | ^7.0 | PHP ≥8.2 |
magicsunday/jsonmapper | ^3.0 | PHP ≥8.3 |
magicsunday/jsonmapper | ^2.4 | PHP ≥8.1 |
phpunit/phpunit | ^12.0 | PHP ≥8.3 |
phpunit/phpunit | ^11.0 | PHP ≥8.2 |
Diagnosis Command
# Find what prevents a specific PHP version
composer why-not php:8.2Fix Pattern: Downgrade dependencies to versions compatible with your minimum PHP:
{
"require": {
"php": "^8.2"
},
"require-dev": {
"symfony/http-client": "^7.0",
"phpunit/phpunit": "^11.0"
}
}CI Version Matrix Recommendations
Test against all supported PHP versions:
# .github/workflows/ci.yml
strategy:
matrix:
php: ['8.2', '8.3', '8.4']Ensure dependencies are compatible with your minimum PHP version in CI.
TYPO3 API Traps (Cross-Version)
Architectural rules and silent footguns that bite across TYPO3 v12, v13, and v14. Each one looks innocuous at the call site but produces wrong-but-not-fatal behavior — soft-deleted records vanish, registrations get silently dropped, paths double, DI breaks at runtime.
---
Connection::select() Applies TCA Restrictions Silently
TYPO3\CMS\Core\Database\Connection::select() (the convenience wrapper, not the QueryBuilder fluent API) applies the default `RestrictionContainer`, which includes DeletedRestriction, HiddenRestriction, and StartTimeRestriction per TCA. Code reaching for "I just want to read the row" misses every soft-deleted / hidden / time-restricted record.
This bites hardest in admin tooling, cleanup scripts, and audit features that explicitly want to see deleted records.
Search Pattern
grep -rn "->select(\|Connection::select" Classes/Fix — drop down to QueryBuilder and remove restrictions explicitly:
// ❌ Silently filters deleted/hidden/time-restricted rows
$rows = $connection->select(['*'], 'be_users', ['uid' => $uid])->fetchAllAssociative();
// ✅ See every row, including deleted
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$rows = $queryBuilder
->select('*')
->from('be_users')
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)))
->executeQuery()
->fetchAllAssociative();To keep some restrictions but drop others, use removeByType(DeletedRestriction::class) instead of removeAll().
---
TYPO3_USER_SETTINGS Registration MUST Live in ext_tables.php
cms-setup's own ext_tables.php rebuilds $GLOBALS['TYPO3_USER_SETTINGS'] from scratch. Any field your extension registers in ext_localconf.php is wiped out before the setup module gets to read it — and the failure mode is silent: no error, the field just doesn't appear.
Affected: any custom user-settings panel, e.g. an "Enable passkey login" toggle.
Symptom: field renders during local dev (when caches are warm with stale data) but is missing on a fresh install / after Flush all caches.
Fix — move the registration:
// ❌ ext_localconf.php — silently overwritten
ExtensionManagementUtility::addUserTSConfig(...);
$GLOBALS['TYPO3_USER_SETTINGS']['columns']['tx_myext_setting'] = [...];
// ✅ ext_tables.php — runs AFTER cms-setup/ext_tables.php
$GLOBALS['TYPO3_USER_SETTINGS']['columns']['tx_myext_setting'] = [...];
ExtensionManagementUtility::addFieldsToUserSettings('tx_myext_setting', 'after:lang');See also: TYPO3 boot order below.
---
TYPO3 Boot Order
The reason for the rule above. Boot order is:
1. ext_localconf.php of every active extension (in extension dependency order) 2. TCA loaded 3. ext_tables.php of every active extension (in extension dependency order)
Anything that depends on another extension's `ext_tables.php` having already executed must itself live in ext_tables.php. The two most common cases:
- User Settings fields —
cms-setup/ext_tables.phpoverwrites$GLOBALS['TYPO3_USER_SETTINGS'] - Backend module overrides — depend on
cms-backendhaving registered the module first
Mental model: ext_localconf.php is for "configure the framework" (DI, caches, hooks, event listeners). ext_tables.php is for "extend things the framework already built."
---
callUserFunction() Bypasses Dependency Injection
GeneralUtility::callUserFunction() instantiates the target class via makeInstance() without DI, calling the constructor with no arguments. Any class used as a userFunc target (TypoScript userFunc, TCA displayCond, custom hooks routed through callUserFunction, user-settings panels, etc.) cannot use constructor injection — you'll get a TypeError: too few arguments.
Search Pattern
grep -rn "callUserFunction\|userFunc\s*=" Classes/ Configuration/Fix options (in order of preference):
// ❌ Constructor DI breaks under callUserFunction
final class MyPanel
{
public function __construct(private readonly LanguageService $lang) {}
public function render(array $params): string { ... }
}// ✅ Option 1 — pull deps via makeInstance() inside the method
final class MyPanel
{
public function render(array $params): string
{
$lang = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences(...);
// ...
}
}// ✅ Option 2 — refactor away from callUserFunction
// For TCA displayCond: use the array-form `displayCond` instead of userFunc
// For TypoScript: use a USER content object pointing at a properly DI'd controller action
// For PSR-14 events: just write an event listenerPHPStan won't catch this — the class constructor is valid PHP. The error appears only when TYPO3 actually invokes the userFunc.
---
TemplatePaths::ensureAbsolutePath() Resolves EXT: Paths Itself
Fluid's TYPO3Fluid\Fluid\View\TemplatePaths::ensureAbsolutePath() (and the layers above it — setTemplateRootPaths, setLayoutRootPaths, setPartialRootPaths) accept EXT:my_ext/Resources/Private/Templates/ directly and resolve it through GeneralUtility::getFileAbsFileName() internally.
If you pre-resolve the path yourself with GeneralUtility::getFileAbsFileName('EXT:my_ext/...') and then pass the absolute result, most setups still work — but on Composer-mode installs where the EXT path resolves to a symlinked vendor directory, you can hit path-doubling (/var/www/html/vendor/.../EXT:my_ext/...) or stale resolution after a composer dump-autoload.
Fix — pass EXT: paths verbatim:
// ❌ Pre-resolved
$view->setTemplateRootPaths([
GeneralUtility::getFileAbsFileName('EXT:my_ext/Resources/Private/Templates/'),
]);
// ✅ Let TemplatePaths resolve it
$view->setTemplateRootPaths([
'EXT:my_ext/Resources/Private/Templates/',
]);Same applies to LayoutRootPaths and PartialRootPaths.
---
See Also
upgrade-v11-to-v12.md— v12 FormEngine DI nodes (setData()workaround for #100670)upgrade-v12-to-v13.md—#[AsEventListener]v13+ vsServices.yamltag for v12 compatupgrade-v13-to-v14.md—LoginProviderInterface::modifyView(),StandaloneViewremoval,ModifyPageLayoutOnLoginProviderSelectionEventsignature driftapi-changes.md— full deprecated/removed API tables per version
TYPO3 Multi-Version Compatibility (v12 + v13 + v14)
When extension must support ^12.4 || ^13.4 || ^14.1.
Version Constraints
{
"require": {
"php": "^8.2",
"typo3/cms-core": "^12.4 || ^13.4 || ^14.1"
}
}// ext_emconf.php
'constraints' => [
'depends' => [
'typo3' => '12.4.0-14.99.99',
'php' => '8.2.0-8.99.99',
],
],Critical: Rector Configuration
Do NOT use `UP_TO_TYPO3_13` - it introduces v13-only APIs that break v12.
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_82,
// ONLY v12 rules for dual compatibility
Typo3LevelSetList::UP_TO_TYPO3_12,
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
]);API Compatibility Matrix
| Purpose | v12 Compatible (use this) | v13 Only (avoid) |
|---|---|---|
| Session access | $TSFE->fe_user->getKey() | $request->getAttribute('frontend.user') |
| Page info | $data['pObj']->rootLine | $request->getAttribute('frontend.page.information') |
Rule: Always use v12-compatible APIs when supporting both versions.
Fluid Template Cross-Version Patterns
f:be.infobox state parameter
The state argument type differs across versions:
| Version | state type | Accepts |
|---|---|---|
| v12 | int | InfoboxViewHelper::STATE_* integer constants |
| v13 | int | InfoboxViewHelper::STATE_* integer constants |
| v14 | mixed | ContextualFeedbackSeverity enum OR integer |
Rule: Always use InfoboxViewHelper::STATE_* constants. Never use ContextualFeedbackSeverity enum for f:be.infobox state — it breaks v12/v13.
Verified: STATE_* constants exist in v12.4, v13.4, and v14.2.
<!-- CORRECT: works on v12/v13/v14 -->
<f:be.infobox title="Note" state="{f:constant(name: 'TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper::STATE_INFO')}">
<!-- WRONG: breaks v12/v13 (state expects int, gets enum object) -->
<f:be.infobox title="Note" state="{f:constant(name: 'TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::INFO')}">Constants: STATE_NOTICE (-2), STATE_INFO (-1), STATE_OK (0), STATE_WARNING (1), STATE_ERROR (2).
Badge CSS classes
Use badge-* classes. TYPO3 core uses badge badge-success etc. in its own backend templates (e.g., MFA overview) across all versions:
<span class="badge badge-success">Active</span>PHP Cross-Version Patterns
IconSize enum (v13+)
| Version | IconFactory::getIcon() $size | Available |
|---|---|---|
| v12 | string (untyped) | Icon::SIZE_SMALL etc. |
| v13 | IconSize enum | Both enum and deprecated constants |
| v14 | IconSize enum (strict) | IconSize::SMALL only |
Pattern: Use enum_exists() with argument unpacking for cross-version compat:
use TYPO3\CMS\Core\Imaging\IconSize;
$icon = $this->iconFactory->getIcon(
'actions-question-circle',
...(\enum_exists(IconSize::class) ? [IconSize::SMALL] : ['small']),
);Add PHPStan ignoreErrors for the mixed-type warning:
# phpstan.neon
parameters:
ignoreErrors:
-
message: '#IconSize#'
path: %currentWorkingDirectory%/Classes/Controller/MyController.php
reportUnmatched: false
-
message: '#expects string, mixed given#'
path: %currentWorkingDirectory%/Classes/Controller/MyController.php
reportUnmatched: falsePHPStan inline ignores
Never use `@phpstan-ignore` in code — CI configs may reject inline ignores. Always use phpstan.neon ignoreErrors with reportUnmatched: false.
Third-Party Dependency Dual Compatibility
The same principles apply to non-TYPO3 dependencies when supporting multiple major versions (e.g., "intervention/image": "^3.0 || ^4.0"):
- Use only APIs that exist in ALL supported versions of the dependency
- Check interface definitions, not just concrete class methods
- Use adapter pattern when APIs differ between major versions
- Run PHPStan and tests against each major version separately
- Never use `@phpstan-ignore` to suppress version-conditional method errors
See third-party-dependency-upgrades.md for detailed patterns and examples.
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Rector v13 breaks v12 | v13-only APIs | Only use UP_TO_TYPO3_12 for dual compat |
$TSFE undefined | Not always set | Use $GLOBALS['TSFE'] ?? null |
| Method not found on interface | Method exists on concrete class but not interface | Use adapter pattern or version-safe API |
@phpstan-ignore masks runtime error | Suppresses analysis but code still fails at runtime | Refactor to adapter pattern |
Mock ->method() fails | Mocked method removed in new version | Mock your own adapter interface instead |
| PHPStan passes but tests fail | PHPStan only checks one installed version | Run against each major version in CI |
Multi-Version Worktrees and Backports
Concrete patterns for upgrading an extension across multiple TYPO3 LTS versions in parallel, and for backporting fixes from main to maintenance branches.
The Rule: One Worktree Per LTS
Never switch branches in place when working on cross-version extension changes. Each TYPO3 LTS target gets its own worktree so:
composer.lockdoesn't get rewritten every time you switch- DDEV/Docker containers keep their state
- Build artifacts don't get clobbered across versions
- You can run tests on v11 and v13 simultaneously in two terminals
Layout
~/projects/<ext-name>/
├── .bare/ # bare git clone — source of truth
├── main/ # default branch (usually latest-supported)
├── TYPO3_11/ # v11 maintenance branch worktree
├── TYPO3_12/ # v12 maintenance branch worktree
├── TYPO3_13/ # v13 maintenance branch worktree
└── feature-XYZ/ # topical work, from whichever base appliesSetup
# Resolve an absolute project root once so every subsequent command is cwd-safe.
EXT=<ext-name>
PROJ="$HOME/projects/$EXT"
BARE="$PROJ/.bare"
mkdir -p "$PROJ"
git clone --bare <repo-url> "$BARE"
git -C "$BARE" config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
# One worktree per supported version. Absolute paths for both -C and dest
# (see 'Absolute Paths Only' below).
git -C "$BARE" worktree add "$PROJ/main" main
git -C "$BARE" worktree add "$PROJ/TYPO3_11" TYPO3_11 # only if v11 is still supported
git -C "$BARE" worktree add "$PROJ/TYPO3_12" TYPO3_12
git -C "$BARE" worktree add "$PROJ/TYPO3_13" TYPO3_13Absolute Paths Only
Creating a worktree with a relative destination path can silently produce the worktree INSIDE .bare/ if the cwd is wrong. Always use absolute paths — both for the -C <bare> argument and for the worktree destination:
# WRONG — relative paths, depends on cwd
git -C .bare worktree add ../feature-fix feature-fix
# RIGHT — absolute paths (use the $BARE / $PROJ variables from Setup)
git -C "$BARE" worktree add "$PROJ/feature-fix" feature-fixIf you find a worktree inside .bare/, prune it with git -C "$BARE" worktree remove <path> (or git worktree remove --force <path> from inside a sibling worktree) and recreate at the correct path. Never run rm -rf on a path under .bare/ — you'll destroy the bare clone.
Cache Safety
Never edit the installed extension under vendor/<vendor>/<ext-name>/ or typo3conf/ext/<ext-name>/. Those are deployed copies. Edit only in the source worktree. Composer sync / deploy will overwrite the installed copy on the next build.
Pre-edit check. Patterns include both …/segment/… (cwd inside the segment) and …/segment (cwd is the segment), so a cwd sitting exactly at the directory boundary is caught too:
pwd_real=$(realpath .)
case "$pwd_real" in
*/vendor/*|*/vendor|*/typo3conf/ext/*|*/typo3conf/ext|*/.bare/*|*/.bare)
echo "REFUSING to edit installed/cache path: $pwd_real"
exit 1
;;
esacBackport Workflow (TYPO3_12 from main)
When to backport vs port
- Backport: the fix is a pure bug fix that applies to both versions with minor API differences.
- Port (rewrite): the fix uses APIs that don't exist in the older version; write the v12 version from scratch rather than force a cherry-pick.
Step-by-step cherry-pick
# 1. Identify the fix commit on main
cd ~/projects/<ext-name>/main
git log --oneline --grep='fix: <bug>' -n 5
# 2. Switch to the maintenance worktree (never branch-switch in place)
cd ~/projects/<ext-name>/TYPO3_12
# 3. Cherry-pick
git cherry-pick <sha>
# 4. Resolve API differences — expect conflicts if the fix used v13-only APIs
# 5. Run tests in the v12 worktree ONLY (not the main worktree)
Build/Scripts/runTests.sh -s unit -p 8.1
Build/Scripts/runTests.sh -s functional -p 8.1
# 6. Amend the cherry-pick commit to document the origin. -s adds a
# Signed-off-by trailer automatically (don't add one by hand — that
# duplicates it); -S re-signs the amended commit.
git commit --amend -s -S -m "fix: <bug>
Backport of <sha> from main."
# 7. Push backport branch
git push -u origin backport/TYPO3_12-<bug>Backport PR conventions
- Target branch: the maintenance branch (
TYPO3_12), notmain. - PR title prefix:
[TYPO3_12]so reviewers see the target at a glance. - Label:
backport. - Release notes: the backport is its own minor or patch release on the maintenance line; not tied to the main-line release number.
Verifying the cherry-pick target
Before pushing:
pwd # must match the target worktree
git branch --show-current # must be backport/TYPO3_12-... branch
cat composer.json | jq '.require."typo3/cms-core"' # must show the target version constraint
Build/Scripts/runTests.sh -s unit -p 8.1 # must pass in this worktreeIf any of these show the main-branch values, you're about to push a backport to the wrong branch. Stop.
Cross-Version CI Patterns
Extension CI should exercise every supported LTS on every PR, not just the one the PR is based on. Matrix example:
jobs:
test:
strategy:
fail-fast: false
matrix:
typo3: ["^12.4", "^13.4", "^14.0"]
php: ["8.1", "8.2", "8.3", "8.4"]
exclude:
- { typo3: "^12.4", php: "8.4" } # document WHY this is excluded
- { typo3: "^14.0", php: "8.1" }A passing test run on v13 does NOT certify v11 or v12. If a PR touches code that runs on multiple LTSes, the matrix must be green on every included cell before declaring "tested".
Declaring Version Coverage in PR Description
Mandatory PR description section:
## Version coverage
- [x] v14 — unit + functional green (link to CI run)
- [x] v13 — unit + functional green (link to CI run)
- [ ] v12 — not tested in this PR; follow-up in #<number>If a version is "not tested", say so explicitly and link a follow-up issue. Silent coverage gaps are the pattern that causes "worked on v13, broke on v11" regressions.
Pre-Upgrade Checklist
Use this checklist before starting a TYPO3 extension upgrade.
Repository Status
- [ ] Git repository is clean (
git statusshows no changes) - [ ] On main/master branch
- [ ] All tests passing on current version
- [ ] CI/CD pipeline green
Current State Assessment
- [ ] Document current TYPO3 version support
- [ ] Document current PHP version requirement
- [ ] List all deprecated API usages (run PHPStan with deprecation rules)
- [ ] Identify database-related code (for DBAL migration)
- [ ] Check for direct
$GLOBALS['TSFE']usage - [ ] Check for
GeneralUtility::_GET/POST/GPusage - [ ] Check for
PDO::PARAM_*constants
Dependency Check
- [ ] Review
composer.jsondependencies - [ ] Check for abandoned packages
- [ ] Verify testing framework compatibility
- [ ] Check Rector/PHPStan version requirements
- [ ] Identify third-party dependencies with major version changes (see
third-party-dependency-upgrades.md) - [ ] For each major-bumped dependency: enumerate all API usages in codebase
- [ ] Cross-reference usages against new version's changelog/upgrade guide
- [ ] Verify methods called on interfaces exist in ALL supported major versions
Backup & Branch
- [ ] Create feature branch:
git checkout -b feature/typo3-v12-v13-upgrade - [ ] Tag current state:
git tag v-before-upgrade - [ ] Ensure local development environment works
Environment Setup
- [ ] DDEV or local environment ready
- [ ] Can install TYPO3 v12.4 LTS
- [ ] Can install TYPO3 v13.4 LTS
- [ ] Database access configured
Documentation Review
- [ ] Read TYPO3 v12 changelog: https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Index.html
- [ ] Read TYPO3 v13 changelog: https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/13.0/Index.html
- [ ] Review breaking changes relevant to extension
Version Hardcoding Locations
Check ALL files with hardcoded PHP/TYPO3 versions before starting:
- [ ]
composer.json(require.php, require.typo3/cms-core) - [ ]
.github/workflows/*.yml(PHP version matrix) - [ ]
.ddev/config.yaml(php_version) - [ ]
Dockerfile,docker-compose.yml - [ ]
rector.php(LevelSetList, SetList) - [ ]
fractor.php(Typo3LevelSetList) - [ ]
phpstan.neon(phpVersion) - [ ]
Build/phpstan/*.neon - [ ]
README.md,Documentation/*.rst(version requirements) - [ ]
ext_emconf.php(constraints)
Planning Phase (Major Upgrades)
When performing major upgrades (PHP version drops, TYPO3 major versions), complete these steps before any code changes:
1. List all files with hardcoded versions (composer.json, CI, Docker, Rector) 2. Document scope - how many places need changes? 3. Present plan to user for approval 4. Track progress with todo list
Understand Your Situation First
Before running any automated tools, answer these questions:
- Current TYPO3 support: What versions does
composer.jsoncurrently support? - Current PHP requirement: What
phpconstraint is incomposer.json? - Test status: Do tests pass on the current version? (If not, fix first)
- Target TYPO3 version(s): Which LTS version(s) will you support?
- Target PHP version(s): What PHP versions must the extension run on?
- Dropping support?: Will you drop support for older TYPO3/PHP versions?
- Business driver: Client requirement? Security? End-of-life support?
- Dependencies: Do other extensions/projects depend on this one?
Risk Assessment
- Does this extension have tests? (No tests = high risk)
- Does it use complex APIs (DBAL, Extbase, Fluid ViewHelpers)?
- Does it hook into TYPO3 internals (PSR-15, signals/slots)?
- How much custom JavaScript/CSS? (May need build system updates)
If any answer raises concerns, document them before proceeding.
Team Communication
- [ ] Inform team about upgrade work
- [ ] Set up review process for changes
- [ ] Plan testing strategy
Real-World Upgrade Patterns
Source: netresearch/contexts extension upgrade from v11 to v12/v13 (2024-12)
Deprecation Patterns Discovered
1. Container::registerImplementation() Removed
v11 Pattern (ext_localconf.php):
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Extbase\Object\Container\Container::class
)->registerImplementation(
\Netresearch\Contexts\Context\AbstractContext::class,
\Netresearch\Contexts\Context\IpContext::class
);v12+ Fix: Remove entirely. Use Services.yaml for DI:
services:
Netresearch\Contexts\Context\IpContext:
public: true2. GeneralUtility::_GET() Deprecated
Search Pattern:
grep -rn "GeneralUtility::_GET\|GeneralUtility::_POST\|GeneralUtility::_GP" Classes/v11 Pattern:
$value = GeneralUtility::_GET('tx_myext');v12+ Fix:
// Option 1: Direct superglobal (for simple cases)
$value = $_GET['tx_myext'] ?? null;
// Option 2: From PSR-7 request (preferred)
$value = $request->getQueryParams()['tx_myext'] ?? null;3. $TSFE Global Undefined
Search Pattern:
grep -rn "\$GLOBALS\['TSFE'\]" Classes/v11 Pattern:
$pageId = $GLOBALS['TSFE']->id;v12 Compatible Fix:
$tsfe = $GLOBALS['TSFE'] ?? null;
$pageId = $tsfe?->id ?? 0;v13 Preferred (with request):
$pageInfo = $request->getAttribute('frontend.page.information');
$pageId = $pageInfo?->getId() ?? 0;4. Doctrine DBAL 4.x createNamedParameter
Search Pattern:
grep -rn "createNamedParameter" Classes/v11 (DBAL 3.x):
$queryBuilder->createNamedParameter($value, \PDO::PARAM_INT);v12+ (DBAL 4.x):
use Doctrine\DBAL\Connection;
$queryBuilder->createNamedParameter($value, Connection::PARAM_INT);Available Constants:
| PDO Constant | DBAL Constant |
|---|---|
PDO::PARAM_INT | Connection::PARAM_INT |
PDO::PARAM_STR | Connection::PARAM_STR |
PDO::PARAM_BOOL | Connection::PARAM_BOOL |
PDO::PARAM_NULL | Connection::PARAM_NULL |
5. itemFormElID Removed in FormEngine
Search Pattern:
grep -rn "itemFormElID" Classes/v11 Pattern:
$elementId = $data['parameterArray']['itemFormElID'];v12+ Fix (generate from itemFormElName):
$elementName = $data['parameterArray']['itemFormElName'];
$elementId = str_replace(['[', ']'], ['_', ''], $elementName);6. xml2array() Null Argument
Issue: GeneralUtility::xml2array() doesn't accept null
v11 Pattern:
$config = GeneralUtility::xml2array($row['config']);v12+ Fix:
$config = !empty($row['config'])
? GeneralUtility::xml2array((string) $row['config'])
: [];SC_OPTIONS Hooks to PSR-14 Events
Common Hook Migrations
| SC_OPTIONS Hook | PSR-14 Event |
|---|---|
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] | AfterRecordOperationEvent |
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] | AfterTypoScriptDeterminedEvent |
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] | Custom middleware or PSR-14 event |
Migration Pattern
v11 Hook (ext_localconf.php):
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= \Vendor\Extension\Hooks\DataHandler::class;v12+ PSR-14 Event (Services.yaml):
services:
Vendor\Extension\EventListener\DataHandlerListener:
tags:
- name: event.listener
event: TYPO3\CMS\Core\DataHandling\Event\AfterRecordOperationEventFractor Migrations Applied
Successfully migrated non-PHP files:
| File Type | Migration |
|---|---|
| FlexForms XML | Structure updates for v12 |
| TypoScript | [end] → [global] |
| Fluid Templates | Namespace updates |
Command:
./vendor/bin/fractor process --dry-run # Preview
./vendor/bin/fractor process # ApplySite Sets for TYPO3 13
New configuration structure:
Configuration/
├── Sets/
│ └── MyExtension/
│ ├── config.yaml
│ ├── settings.yaml
│ └── setup.typoscriptconfig.yaml:
name: vendor/my-extension
label: My Extension
dependencies:
- typo3/fluid-styled-contentCI Matrix Pattern for Dual v12/v13 Support
# .github/workflows/ci.yml
jobs:
test:
strategy:
matrix:
typo3: ['12.4', '13.4']
php: ['8.2', '8.3', '8.4']
exclude:
- typo3: '12.4'
php: '8.4' # v12 max PHP 8.3Third-Party Dependency Major Version Upgrades
When composer.json constraints widen to include a new major version of ANY dependency (not just TYPO3 core), the upgrade requires systematic API compatibility validation.
When This Applies
composer.jsonchanges from"vendor/package": "^3.0"to"^3.0 || ^4.0"- A dependency releases a new major version with breaking changes
- Multi-version support is required (e.g., intervention/image v3 + v4)
Workflow: Third-Party Dependency Upgrade
Step 1: Enumerate All Usages
Search your extension's PHP code (e.g., Classes/, Tests/, Configuration/, Resources/) for ALL usages of the dependency's API:
# Find all imports/use statements for the package namespace
grep -rn "use Vendor\\Package\\" Classes/ Tests/ Configuration/ Resources/
# Find all method calls on objects of that type
grep -rn "->methodName(" Classes/ Tests/ Configuration/ Resources/
# Find all static calls
grep -rn "Vendor\\Package\\ClassName::" Classes/ Tests/ Configuration/ Resources/Step 2: Cross-Reference Against New Version's API
For each usage found, verify the method/class still exists in the new major version:
1. Check the package's UPGRADE.md or CHANGELOG for breaking changes 2. Read the interface definitions in both versions — interfaces are the contract 3. Compare method signatures (parameter types, return types, parameter order) 4. Check for renamed/removed classes (namespace changes are common in major bumps)
Step 3: Flag Interface vs Concrete Class Methods
Critical pitfall: Methods called on interface-typed variables must exist on the interface in ALL supported versions, not just on the concrete class.
// WRONG: toWebp() exists on ImageInterface in v3 but NOT in v4
public function process(ImageInterface $image): ImageInterface
{
return $image->toWebp()->save($path); // Breaks on v4
}
// RIGHT: save() accepts format parameter in v4, use version-safe API
public function process(ImageInterface $image): ImageInterface
{
return $image->save($path); // Works on both v3 and v4
}Validation rule: For every ->method() call, check:
- Is the variable typed to an interface or a concrete class?
- Does the method exist on the interface (not just the concrete implementation)?
- Does the method exist on the interface in ALL supported major versions?
Step 4: Adapter Pattern for Incompatible APIs
When method signatures differ between major versions, use an adapter:
// Adapter interface (your code)
interface ImageProcessorInterface
{
public function convertToWebp(object $image, string $path): void;
}
// v3 adapter
class ImageProcessorV3 implements ImageProcessorInterface
{
public function convertToWebp(object $image, string $path): void
{
$image->toWebp()->save($path);
}
}
// v4 adapter
class ImageProcessorV4 implements ImageProcessorInterface
{
public function convertToWebp(object $image, string $path): void
{
$image->save($path); // v4 handles format via file extension
}
}
// Factory selects adapter based on installed version
class ImageProcessorFactory
{
public static function create(): ImageProcessorInterface
{
if (method_exists(ImageInterface::class, 'toWebp')) {
return new ImageProcessorV3();
}
return new ImageProcessorV4();
}
}Step 5: Version Detection Pitfalls
Do NOT use `method_exists()` directly in business logic — PHPStan narrows the type and can cause false positives/negatives:
// WRONG: PHPStan narrows $image type after method_exists()
if (method_exists($image, 'toWebp')) {
$image->toWebp()->save($path); // PHPStan may still error
} else {
$image->save($path);
}
// RIGHT: Use adapter pattern with `object` type parameter
// or check on the class/interface name, not the instance:
if (method_exists(ImageInterface::class, 'toWebp')) {
// v3 path
} else {
// v4 path
}PHPStan Multi-Version Validation
Problem
PHPStan analyzes code against ONE version of installed dependencies at a time. When you support ^3.0 || ^4.0, PHPStan with v4 installed will flag v3-only method calls, and vice versa.
Solution: Run PHPStan Against Each Major Version
# Test with v3
composer require vendor/package:^3.0 --no-interaction
./vendor/bin/phpstan analyse
# Test with v4
composer require vendor/package:^4.0 --no-interaction
./vendor/bin/phpstan analyseCI Matrix for Multi-Version Dependencies
# .github/workflows/ci.yml
jobs:
phpstan:
strategy:
matrix:
include:
- dependency-version: "^3.0"
label: "vendor/package v3"
- dependency-version: "^4.0"
label: "vendor/package v4"
steps:
- run: composer require vendor/package:${{ matrix.dependency-version }} --no-interaction
- run: ./vendor/bin/phpstan analyse@phpstan-ignore Tags Are Version-Specific
@phpstan-ignore annotations suppress errors for ONE version but the suppressed code may itself be invalid in another version:
// WRONG: Suppresses the error when v4 is installed, but the code
// itself fails at runtime with v4 because toWebp() doesn't exist
/** @phpstan-ignore method.notFound */
$image->toWebp()->save($path);
// RIGHT: Use adapter pattern so no @phpstan-ignore is needed at all
$this->imageProcessor->convertToWebp($image, $path);Rule: If you find yourself adding @phpstan-ignore for version-conditional code, refactor to the adapter pattern instead. The ignore tag masks a real runtime error in one of the supported versions.
Test Compatibility for Multi-Version Dependencies
Mock Methods Must Exist on Interfaces
When mocking dependency interfaces, every ->method('foo') call must reference a method that exists on the interface in ALL supported versions:
// WRONG: toWebp() is not on ImageInterface in v4
$mock = $this->createMock(ImageInterface::class);
$mock->method('toWebp')->willReturn($encodedImage);
// RIGHT: Mock only methods that exist on the interface in all versions
// Or mock your own adapter interface instead
$mock = $this->createMock(ImageProcessorInterface::class);
$mock->expects($this->once())
->method('convertToWebp');Mock Callback Signatures Must Match
When using willReturnCallback(), the callback signature must match the method signature in the version being tested:
// If save() signature changed between v3 and v4:
// v3: save(string $path): self
// v4: save(string $path, string $format = null, int $quality = 90): self
// The mock callback must be compatible with both:
$mock->method('save')->willReturnCallback(
function (string $path, ...$args) use ($mock) {
// Handle both signatures via variadic
return $mock;
}
);Maintaining Test Specificity
When refactoring from version-specific APIs (e.g., ->toWebp()->save()) to version-agnostic APIs (e.g., ->save()), assertions must remain equally specific:
// BEFORE: Specific assertion on toWebp() call chain
$mock->expects($this->once())->method('toWebp');
$encodedMock->expects($this->once())->method('save')->with($outputPath);
// AFTER (WRONG): Lost specificity - just asserts save() was called
$mock->expects($this->once())->method('save')->with($outputPath);
// AFTER (RIGHT): Assert the output format/path is correct
$mock->expects($this->once())->method('save')
->with(
$this->callback(fn($path) => str_ends_with($path, '.webp')),
// Additional assertions on format parameters if applicable
);Checklist: Third-Party Dependency Upgrade
- [ ] Identified all usages of dependency API in
Classes/andTests/ - [ ] Cross-referenced each usage against new version's changelog/upgrade guide
- [ ] Verified all method calls exist on interfaces (not just concrete classes)
- [ ] Used adapter pattern where method signatures differ between versions
- [ ] No
@phpstan-ignoretags for version-conditional code (use adapters) - [ ] PHPStan passes with EACH supported major version installed
- [ ] Test mocks only reference methods on interfaces valid in ALL versions
- [ ] CI matrix tests against each supported major version
- [ ] Mock callbacks match method signatures for all supported versions
Understanding Rector/Fractor Output
CRITICAL: Always run with `--dry-run` first and review the output before applying changes.
Reading Rector Dry-Run Output
./vendor/bin/rector process --dry-runThe output shows:
- File path: Which file will be modified
- Rule name: Which Rector rule triggered (e.g.,
ExtbaseControllerActionsMustReturnResponseInterfaceRector) - Diff: Exact changes that will be made (red = removed, green = added)
Before applying, check: 1. Does the rule apply correctly to your code context? 2. Are there edge cases Rector might miss? 3. Will this break dual-version compatibility? (See dual-compatibility.md)
Reading Fractor Dry-Run Output
./vendor/bin/fractor process --dry-runFractor modifies non-PHP files. Watch for:
- TypoScript: Removed/renamed options
- FlexForms: Changed XML structures
- YAML configurations: Service definitions, routes
Troubleshooting
Rector Broke My Code
If Rector applied changes that broke the extension:
Immediate Recovery
# Option 1: Revert all Rector changes
git checkout -- .
# Option 2: Revert specific files
git diff --name-only | xargs git checkout --
# Option 3: If already committed
git revert HEADDiagnose the Problem
1. Run Rector on a single file to isolate: ./vendor/bin/rector process path/to/file.php --dry-run 2. Check which rule caused the issue (look at rule name in output) 3. Exclude problematic rules in rector.php:
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->skip([
// Skip a specific rule globally
\Ssch\TYPO3Rector\Rector\v12\SomeProblematicRector::class,
// Skip a rule for specific files
\Ssch\TYPO3Rector\Rector\v12\SomeRector::class => [
__DIR__ . '/Classes/Problematic.php',
],
]);
};Common Rector Failures
- Extbase action return types: May break if controller has custom response handling
- Dependency injection: May fail with complex factory patterns
- Signal/slot to PSR-14: Requires manual event class creation
PHPStan Errors After Upgrade
If PHPStan reports many errors after Rector:
1. Baseline approach: Create a baseline for pre-existing issues:
./vendor/bin/phpstan analyse --generate-baseline2. Incremental fix: Fix errors file by file, not all at once
3. Common post-upgrade errors:
- Missing return types (add them manually)
- Deprecated method calls Rector missed (check changelog)
- Type mismatches from changed TYPO3 APIs
PHPStan with Multi-Version Dependencies
When supporting multiple major versions of a dependency (e.g., "vendor/package": "^3.0 || ^4.0"):
1. PHPStan only sees ONE version at a time — it analyzes against whatever is installed 2. Run PHPStan against EACH major version separately (in a disposable worktree/CI job, or restore composer files after each run):
composer require vendor/package:^3.0 --no-interaction && ./vendor/bin/phpstan analyse
git restore composer.json composer.lock
composer require vendor/package:^4.0 --no-interaction && ./vendor/bin/phpstan analyse
git restore composer.json composer.lock3. `@phpstan-ignore` is NOT a solution for version-conditional code:
- The tag suppresses the error but the code may still fail at runtime
- Example:
@phpstan-ignore method.notFoundon$image->toWebp()hides the error
with v4 installed, but toWebp() will throw at runtime with v4
- Use the adapter pattern instead (see
third-party-dependency-upgrades.md)
4. `method_exists()` gets narrowed by PHPStan: After method_exists($obj, 'foo'), PHPStan narrows the type and may still produce errors in the else branch. Use method_exists(ClassName::class, 'foo') on the class/interface name instead, or better yet, use the adapter pattern with object type parameters.
Tests Fail After Upgrade
1. Identify scope: How many tests fail? All? Some? 2. Check test framework: Is typo3/testing-framework compatible with target TYPO3? 3. Check test fixtures: Do fixtures use deprecated APIs? 4. Update test bootstrap: May need new bootstrap for changed TYPO3 internals
Test Failures from Multi-Version Dependencies
When tests fail after widening a dependency's version constraint:
1. Mock methods must exist on interfaces in ALL versions: If a mock calls ->method('foo') and foo() was removed from the interface in the new version, the mock setup itself may fail or produce incorrect behavior.
2. Mock callback signatures must match: willReturnCallback() closures must accept parameters compatible with the method signature in the version under test. Use variadic parameters (...$args) to handle signature differences.
3. Don't lose test specificity during refactoring: When replacing ->toWebp()->save() with ->save(), the assertion must remain equally specific. Assert on output format, file extension, or other indicators rather than just asserting save() was called.
4. Run tests against each dependency version (in a disposable worktree/CI job, or restore composer files after each run):
composer require vendor/package:^3.0 --no-interaction && ./vendor/bin/phpunit
git restore composer.json composer.lock
composer require vendor/package:^4.0 --no-interaction && ./vendor/bin/phpunit
git restore composer.json composer.lockExtension Installs But Doesn't Work
If the extension installs without errors but functionality is broken:
1. Check backend logs: TYPO3 Admin Tools > Log 2. Check PHP error log: Often reveals missing classes/methods 3. Clear all caches: Admin Tools > Maintenance > Flush TYPO3 and PHP Caches 4. Verify database: Extension Manager may need to update database schema
TYPO3 v11 to v12 Upgrade Guide
Version Constraints
{
"require": {
"php": "^8.1",
"typo3/cms-core": "^12.4"
}
}// ext_emconf.php
'constraints' => [
'depends' => [
'typo3' => '12.4.0-12.4.99',
'php' => '8.1.0-8.4.99',
],
],Rector Configuration
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_81,
Typo3LevelSetList::UP_TO_TYPO3_12,
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
]);Key Breaking Changes
| Change | Search | Fix |
|---|---|---|
| Doctrine DBAL 4.x | grep -rn "PDO::PARAM_" | Use Connection::PARAM_* |
| GeneralUtility::_GET/POST | grep -rn "GeneralUtility::_GET" | Use $_GET['param'] ?? null |
| TCA required flag | grep -rn "'eval'.*'required'" | Use 'required' => true |
| itemFormElID removed | grep -rn "itemFormElID" | Generate from itemFormElName |
| FlexForm structure | Run Fractor | Fractor auto-fixes |
| TypoScript [end] | Run Fractor | Changed to [global] |
Composer Dependencies
{
"require-dev": {
"a9f/typo3-fractor": "^0.4",
"friendsofphp/php-cs-fixer": "^3.64",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^11.0",
"rector/rector": "^2.0",
"ssch/typo3-rector": "^3.0",
"typo3/testing-framework": "^9.0"
}
}v12-Specific Gotchas
FormEngine DI Nodes Need Their Own setData()
Source: Deprecation #100670 — DI-aware FormEngine nodes
In v12, AbstractNode::setData() is commented out and the constructor-based NodeFactory invocation is the deprecated path. If your custom FormEngine element (AbstractFormElement subclass) uses constructor injection, NodeFactory falls back to the legacy __construct(NodeFactory $nodeFactory, array $data) path and you get:
TypeError: Argument #1 ($nodeFactory) of MyFormElement::__construct() must be of type NodeFactory, …Fix (works on v12, v13, v14):
final class MyFormElement extends AbstractFormElement
{
public function __construct(
private readonly LanguageService $languageService,
) {}
// REQUIRED on v12 — restored signature without NodeFactory
public function setData(array $data): void
{
$this->data = $data;
}
public function render(): array { /* ... */ }
}# Configuration/Services.yaml — MUST be public for makeInstance lookup
services:
Vendor\MyExt\FormEngine\MyFormElement:
public: true#[Autoconfigure(public: true)] on the class works too. Without public: true, the DI container won't resolve the class for NodeFactory's lazy lookup and you fall back to the broken legacy path.
See also: api-changes.md for detailed patterns.
TYPO3 v12 to v13 Upgrade Guide
Version Constraints
{
"require": {
"php": "^8.2",
"typo3/cms-core": "^13.4"
}
}// ext_emconf.php
'constraints' => [
'depends' => [
'typo3' => '13.4.0-13.4.99',
'php' => '8.2.0-8.4.99',
],
],Rector Configuration
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_82,
Typo3LevelSetList::UP_TO_TYPO3_13,
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
]);Key Breaking Changes
| Change | v12 API | v13 API |
|---|---|---|
| Frontend user | $TSFE->fe_user | $request->getAttribute('frontend.user') |
| Page info | $data['pObj']->rootLine | $request->getAttribute('frontend.page.information') |
Dual v12 + v13 Gotchas
#[AsEventListener] Attribute Is v13+ Only
The PHP attribute #[AsEventListener] was introduced in TYPO3 v13 (Symfony EventDispatcher attribute). If your extension supports ^12.4 || ^13.4 || ^14.0, the attribute alone is not enough — on v12, the listener is never registered (silent: no error, the event simply never fires).
Fix: keep BOTH the attribute AND the Services.yaml tag. v13+ ignores the tag when the attribute is present, so there is no double-registration.
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(identifier: 'vendor-myext/login-listener')]
final class LoginListener
{
public function __invoke(BeforeUserLogoutEvent $event): void { /* ... */ }
}# Configuration/Services.yaml — required for v12 compat
services:
Vendor\MyExt\EventListener\LoginListener:
tags:
- name: event.listener
identifier: 'vendor-myext/login-listener'
event: TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEventDrop the `Services.yaml` tag only when bumping the floor to ^13.4.
See also: api-changes.md for detailed patterns.
Verification & Success Criteria
An upgrade is complete when ALL of these are verified.
Tool Verification
- [ ]
rector process --dry-runshows no changes - [ ]
fractor process --dry-runshows no changes - [ ]
phpstan analysepasses without errors - [ ]
php-cs-fixer fix --dry-runshows no changes - [ ] All unit tests pass
- [ ] All functional tests pass (if any)
Multi-Version Dependency Verification
If composer.json supports multiple major versions of any dependency:
- [ ]
phpstan analysepasses with each supported major version installed - [ ]
phpunitpasses with each supported major version installed - [ ] No
@phpstan-ignoretags used for version-conditional code (use adapters) - [ ] All mock
->method()calls reference methods on interfaces valid in ALL versions - [ ] CI matrix tests against each supported major version combination
Real-World Testing (Required)
Do NOT skip this step. Automated tools cannot catch all issues.
1. Create a fresh TYPO3 instance matching target version:
# Example with DDEV
ddev config --project-type=typo3 --php-version=8.3
ddev start
ddev composer create typo3/cms-base-distribution:^13.42. Install the upgraded extension via Composer (from local path or packagist)
3. Verify core functionality:
- [ ] Extension installs without errors
- [ ] Backend module loads (if applicable)
- [ ] Frontend plugin renders (if applicable)
- [ ] All content elements work (if applicable)
- [ ] Form finishers execute (if applicable)
- [ ] Scheduled tasks run (if applicable)
4. Check browser console for JavaScript errors
5. Test with real content if possible (import from existing site)
Documentation Updated
- [ ]
README.mdreflects new version requirements - [ ]
CHANGELOG.mddocuments the upgrade - [ ]
composer.jsonconstraints are correct