
Typo3 Conformance
- 30 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
Run a scored TYPO3 extension conformance audit against coding, architecture, and testing standards to check TER readiness and v14 modernization.
About
This skill evaluates TYPO3 extensions against coding standards, architecture patterns, and best practices to produce a scored conformance report. A developer uses it for extension quality audits, TER readiness, and modernization to v12/v13/v14.
- Scored conformance checks across metadata, structure, coding, architecture, and testing
- Targets TYPO3 v14.3 LTS gold standard with TER-readiness and modernization audits
Typo3 Conformance by the numbers
- 30 all-time installs (skills.sh)
- Ranked #667 of 1,352 Code Review & Quality 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-conformanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Run a scored TYPO3 extension conformance audit against coding, architecture, and testing standards to check TER readiness and v14 modernization.
Files
TYPO3 Extension Conformance Checker
Evaluate TYPO3 extensions against TYPO3 coding standards, architecture patterns, and best practices.
When to Use
- Extension quality / TER readiness
- Scored conformance reports
- Modernization to v12/v13/v14 (v14.3 LTS is default/gold standard)
Delegation
Testing -> typo3-testing | Docs -> typo3-docs | OpenSSF -> enterprise-readiness
Workflow
Step 0: Context
Read ext_emconf.php + composer.json for version, type, scope.
Steps 1-11: Checks
1. Metadata -- key, TYPO3 version, type 2. Structure -- composer.json, ext_emconf.php, Classes/, Configuration/, Resources/ 3. Coding -- strict_types, PSR-12, PHP 8.4 explicit nullable, PHP 8.5 float-to-int 4. Prohibited -- no $GLOBALS, no GeneralUtility::makeInstance() for services 5. Architecture -- constructor DI, Services.yaml, PSR-14 events (try/catch), PSR-3 logging (LoggerAware+NullLogger), factory fallback 6. Backend -- ES6, Modal API, CSRF, CSP (v13+) 7. Testing -- PHPUnit, Playwright E2E, coverage >70% 8. Practices -- DDEV, runTests.sh, CI/CD 9. TER -- publish workflow, upload comment 10. Audit -- PHPStan baseline, TCA searchFields, XLIFF, cache has()+get(), query properties, multi-version adapters 11. v14 readiness -- no ext_tables.php/HashService/magic finders; Fluid VHs strict; XLF 2-space. See references/v14-deprecations.md.
Step 12: Verify
Re-run after fixes. Document score delta.
Quick Grep Recipes
grep -rL 'strict_types' Classes/ --include='*.php' # missing strict_types
grep -rn '\$GLOBALS' Classes/ --include='*.php' # prohibited $GLOBALS
grep -rn 'GeneralUtility::makeInstance' Classes/ --include='*.php' # makeInstance for services
grep -rPn '\(\s*[A-Za-z\\]+\s+\$\w+\s*=\s*null' Classes/ --include='*.php' | grep -v '?' # PHP 8.4 implicit nullable
grep -rn '->has(' Classes/ --include='*.php' # cache has()+get() anti-pattern
grep -l 'strict_types' ext_emconf.php # ext_emconf must NOT have strict_types
grep -rn '(int)\s*\$' Classes/ --include='*.php' # PHP 8.5 implicit float-to-int
grep -rn 'data-toggle\|data-dismiss\|data-ride' Resources/ --include='*.html' # Bootstrap 4 in Fluid
grep -rn 'HashService\|GeneralUtility::hmac(\|->findBy[A-Z]\|->findOneBy[A-Z]\|->countBy[A-Z]' Classes/ --include='*.php' # v14 removals
[ -f ext_tables.php ] && echo "WARN: ext_tables.php deprecated (#109438)" # v14.3 deprecationScoring
Base (0-100): Architecture(20) + Guidelines(20) + PHP(20) + Testing(20) + Practices(20). Excellence bonus up to 22. Critical issues block regardless.
| Range | Level | Action |
|---|---|---|
| 90+ | Excellent | Production/TER ready |
| 80-89 | Good | Minor fixes |
| 70-79 | Acceptable | Fix before release |
| 50-69 | Needs Work | Significant effort |
| <50 | Critical | Block deployment |
References
See references/:
- Architecture & code:
extension-architecture.md,directory-structure.md,php-architecture.md,coding-guidelines.md,best-practices.md,hooks-and-events.md - Validation:
composer-validation.md,ext-emconf-validation.md,ext-files-validation.md,runtests-validation.md,version-requirements.md,testing-standards.md - Multi-version:
dual-version-compatibility.md(v12+v13),v13-v14-dual-compatibility.md(v13+v14),multi-version-dependency-compatibility.md,v13-deprecations.md,v14-deprecations.md - Practices & environment:
development-environment.md(DDEV) - Backend & publishing:
backend-module-v13.md,ter-publishing.md,report-template.md,excellence-indicators.md,localization-coverage.md,crowdin-integration.md
Asset templates in assets/Build/: PHPStan, PHP-CS-Fixer, Rector, ESLint, Stylelint, TypoScript lint.
---
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-conformance-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
name: Publish new extension version to TER
on:
release:
types: [published]
jobs:
publish:
name: Publish new version to TER
runs-on: ubuntu-latest
env:
TYPO3_EXTENSION_KEY: ${{ secrets.TYPO3_EXTENSION_KEY }}
TYPO3_API_TOKEN: ${{ secrets.TYPO3_TER_ACCESS_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Validate tag format
run: |
if ! [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "::error::Invalid tag format '${GITHUB_REF_NAME}'. Expected format: v1.2.3"
exit 1
fi
- name: Extract version
id: version
run: |
# Strip 'v' prefix for TER (expects "3.0.1" not "v3.0.1")
VERSION="${GITHUB_REF_NAME#v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Extracted version: ${VERSION}"
- name: Prepare release comment
id: comment
env:
RELEASE_BODY: ${{ github.event.release.body }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_URL: ${{ github.event.release.html_url }}
run: |
# TER Upload Comment Format:
# - Plain text only (no HTML/Markdown)
# - Newlines ARE supported (rendered as <br> on frontend)
# - Allowed chars: word chars, whitespace, " % & [ ] ( ) . , ; : / ? { } ! $ - @
# - Stripped in XML export: # * + = ~ ^ | \ < >
# See: references/ter-publishing.md
if [[ -n "${RELEASE_BODY}" ]]; then
# Preserve newlines (TER displays them as line breaks)
# Limit to reasonable length (1000 chars)
COMMENT=$(echo "${RELEASE_BODY}" | head -c 1000)
elif [[ -n "${RELEASE_NAME}" ]]; then
COMMENT="${RELEASE_NAME}"
else
COMMENT="Release ${{ steps.version.outputs.version }}"
fi
# Strip characters not supported in TER XML export
COMMENT="${COMMENT//[#*+=~^|\\<>]/}"
# Append release link on new line
COMMENT="${COMMENT}"$'\n\n'"Details: ${RELEASE_URL}"
# Set GitHub Actions output (HEREDOC preserves newlines)
{
echo "comment<<EOF"
echo "${COMMENT}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
extensions: intl, mbstring, json, zip, curl
tools: composer:v2
- name: Install tailor
run: composer global require typo3/tailor --prefer-dist --no-progress
- name: Publish to TER
run: |
TAILOR="$(composer global config bin-dir --absolute)/tailor"
"${TAILOR}" set-version "${{ steps.version.outputs.version }}"
"${TAILOR}" ter:publish --comment "${{ steps.comment.outputs.comment }}" "${{ steps.version.outputs.version }}"
<?php
declare(strict_types=1);
/**
* Composer Unused Configuration - TYPO3 Extension
*
* This configuration helps identify unused Composer dependencies.
*
* Some TYPO3 packages may be reported as unused even though they're required
* (e.g., typo3/cms-fluid may not show explicit usage but is needed at runtime).
* Add such packages to the filter list below.
*/
use ComposerUnused\ComposerUnused\Configuration\Configuration;
use ComposerUnused\ComposerUnused\Configuration\NamedFilter;
return static function (Configuration $config): Configuration {
// Add packages that should be ignored during unused checks
// These are typically TYPO3 system extensions or runtime dependencies
// Example: typo3/cms-fluid is often required but not directly referenced in code
$config->addNamedFilter(NamedFilter::fromString('typo3/cms-fluid'));
// Add more as needed for your extension:
// $config->addNamedFilter(NamedFilter::fromString('typo3/cms-frontend'));
// $config->addNamedFilter(NamedFilter::fromString('typo3/cms-extbase'));
return $config;
};
{
"$schema": "https://json.schemastore.org/eslintrc",
"extends": ["eslint:recommended"],
"env": {
"browser": true,
"es2021": true
},
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"no-console": "warn",
"no-debugger": "error",
"no-alert": "warn",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"prefer-const": "error",
"no-var": "error"
},
"ignorePatterns": [
"node_modules/",
".Build/",
"*.min.js"
]
}
<?php
declare(strict_types=1);
/**
* PHP-CS-Fixer Configuration - TYPO3 Extension
* Based on TYPO3 Best Practices: https://github.com/TYPO3BestPractices/tea
*
* This configuration uses the official TYPO3 Coding Standards with parallel execution.
*/
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use TYPO3\CodingStandards\CsFixerConfig;
$config = CsFixerConfig::create();
// Enable parallel execution for faster performance
// Automatically detects available CPU cores
$config->setParallelConfig(ParallelConfigFactory::detect());
// Define which directories to check
$config->getFinder()
->in('Classes')
->in('Configuration')
->in('Tests')
// CRITICAL: Exclude ext_emconf.php - TYPO3 does NOT want declare(strict_types=1) in this file
// The ext_emconf.php is processed by TYPO3's extension manager in a special context
// Adding strict_types breaks extension installation/updates
->notName('ext_emconf.php');
// Optionally add more directories:
// ->in('ext_localconf.php')
// ->in('ext_tables.php')
return $config;
# PHPStan Baseline - TYPO3 Extension
# This file contains known issues that are temporarily accepted.
# Regenerate with: composer phpstan:baseline
#
# IMPORTANT: The baseline should SHRINK over time, not grow.
# New code should NOT add to the baseline.
parameters:
ignoreErrors:
# Example baseline entry (delete this and add your actual errors)
# -
# message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|null given\\.$#"
# count: 1
# path: ../Classes/Service/MyService.php
# PHPStan Configuration - TYPO3 Extension
# Based on TYPO3 Best Practices: https://github.com/TYPO3BestPractices/tea
#
# This configuration enforces:
# - Level 10 (bleeding-edge strictness with future PHPStan features)
# - 100% type coverage for parameters and return types
# - Cognitive complexity limits
# - Security-focused disallowed calls and superglobals
includes:
- phpstan-baseline.neon
- ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon
- ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon
- ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-insecure-calls.neon
- ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon
parameters:
# Minimum PHP version (adjust based on your ext_emconf.php)
phpVersion: 80200 # PHP 8.2+
# Parallel execution (optimize for your CI environment)
parallel:
# Don't be overly greedy on machines with more CPUs
maximumNumberOfProcesses: 5
# Analysis level (0-10, higher is stricter)
level: 10
# Paths to analyze
paths:
- ../../Classes
- ../../Configuration
- ../../Tests
- ../../ext_localconf.php
# - ../../ext_tables.php # Uncomment if you still use this (deprecated)
# Type coverage enforcement - ensures type safety
type_coverage:
return_type: 100 # All functions must have return types
param_type: 100 # All parameters must have types
property_type: 95 # 95% of properties must have types
# Cognitive complexity limits - prevents over-complex code
cognitive_complexity:
class: 10 # Maximum complexity per class
function: 5 # Maximum complexity per function
# Type perfection - enforce best practices for type usage
type_perfect:
no_mixed_property: true # Disallow mixed types in properties
no_mixed_caller: true # Disallow mixed types in callers
null_over_false: true # Prefer null over false
narrow_param: true # Use most specific parameter types
narrow_return: true # Use most specific return types
# Security: Disallow debugging functions in production code
disallowedFunctionCalls:
-
function:
- 'var_dump()'
- 'xdebug_break()'
- 'debug()'
- 'dd()'
- 'dump()'
message: 'Use logging instead or remove if it was for debugging purposes.'
-
function: 'header()'
message: 'Use PSR-7 API instead (ResponseInterface::withHeader())'
-
function:
- 'print_r()'
- 'var_export()'
message: 'Use logging instead for production code'
# Security: Disallow TYPO3 debugging utilities
disallowedStaticCalls:
-
method:
- 'TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump()'
- 'TYPO3\CMS\Core\Utility\DebugUtility::debug()'
message: 'Use logging instead or remove if it was for debugging purposes.'
# Security: Enforce PSR-7 - disallow superglobals
disallowedSuperglobals:
-
superglobal:
- '$_GET'
- '$_POST'
- '$_FILES'
- '$_SERVER'
- '$_COOKIE'
- '$_REQUEST'
message: 'Use PSR-7 ServerRequestInterface instead. Access via $request->getQueryParams(), $request->getParsedBody(), etc.'
# Ignore known PHPUnit false positives
ignoreErrors:
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#'
path: '../../Tests/'
# Add your project-specific ignores here
# -
# message: '#specific error pattern#'
# path: '../../Classes/Specific/File.php'
<?php
declare(strict_types=1);
/**
* Rector Configuration - TYPO3 Extension
* Based on TYPO3 Best Practices: https://github.com/TYPO3BestPractices/tea
*
* This configuration enables:
* - Automated TYPO3 migrations (version upgrades)
* - PHP modernization (up to PHP 8.1+)
* - PHPUnit test modernization
* - Code quality improvements
* - ExtEmConf automatic maintenance
*/
use Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector;
use Rector\CodeQuality\Rector\Ternary\SwitchNegatedTernaryRector;
use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
use Rector\ValueObject\PhpVersion;
use Ssch\TYPO3Rector\CodeQuality\General\ConvertImplicitVariablesToExplicitGlobalsRector;
use Ssch\TYPO3Rector\CodeQuality\General\ExtEmConfRector;
use Ssch\TYPO3Rector\Configuration\Typo3Option;
use Ssch\TYPO3Rector\Set\Typo3LevelSetList;
use Ssch\TYPO3Rector\Set\Typo3SetList;
use Ssch\Typo3RectorTestingFramework\Set\TYPO3TestingFrameworkSetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/../../Classes/',
__DIR__ . '/../../Configuration/',
__DIR__ . '/../../Tests/',
__DIR__ . '/../../ext_emconf.php',
__DIR__ . '/../../ext_localconf.php',
// __DIR__ . '/../../ext_tables.php', // Uncomment if you still use this (deprecated)
])
// Minimum PHP version your extension supports
->withPhpVersion(PhpVersion::PHP_82)
// Enable all PHP sets for modernization
->withPhpSets(
true
)
// Note: We're enabling specific sets by default.
// You can temporarily enable more sets as needed for larger refactorings.
->withSets([
// Rector Core Sets (uncomment as needed for major refactorings)
// LevelSetList::UP_TO_PHP_81,
// SetList::CODE_QUALITY,
// SetList::CODING_STYLE,
// SetList::DEAD_CODE,
// SetList::EARLY_RETURN,
// SetList::TYPE_DECLARATION,
// PHPUnit Sets - modernize tests
PHPUnitSetList::PHPUNIT_100,
// PHPUnitSetList::PHPUNIT_CODE_QUALITY,
// TYPO3 Sets - CRITICAL for TYPO3 migrations
// https://github.com/sabbelasichon/typo3-rector/blob/main/src/Set/Typo3LevelSetList.php
// https://github.com/sabbelasichon/typo3-rector/blob/main/src/Set/Typo3SetList.php
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
// TYPO3 Version Migration - ADJUST TO YOUR TARGET VERSION
Typo3LevelSetList::UP_TO_TYPO3_12, // Change to UP_TO_TYPO3_13 when upgrading
// TYPO3 Testing Framework (if using typo3/testing-framework)
// TYPO3TestingFrameworkSetList::TYPO3_TESTING_FRAMEWORK_7,
])
// To have a better analysis from PHPStan, we teach it here some more things
->withPHPStanConfigs([
Typo3Option::PHPSTAN_FOR_RECTOR_PATH,
])
// Additional useful rules
->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
ConvertImplicitVariablesToExplicitGlobalsRector::class,
])
// Auto-import class names (removes need for full namespaces)
->withImportNames(true, true, false)
// ExtEmConfRector: Automatically maintains ext_emconf.php
->withConfiguredRule(ExtEmConfRector::class, [
// Adjust these constraints to match your extension requirements
ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.2.0-8.5.99',
ExtEmConfRector::TYPO3_VERSION_CONSTRAINT => '12.4.0-12.4.99', // or '13.0.0-13.99.99'
ExtEmConfRector::ADDITIONAL_VALUES_TO_BE_REMOVED => [],
])
// Skip specific rules if they cause issues
->withSkip([
// Example: Skip specific rules
// ExplicitBoolCompareRector::class,
// SwitchNegatedTernaryRector::class,
// Example: Skip specific paths
// ExplicitBoolCompareRector::class => [
// __DIR__ . '/../../Classes/Legacy/',
// ],
]);
{
"$schema": "https://json.schemastore.org/stylelintrc",
"extends": "stylelint-config-standard",
"rules": {
"indentation": 2,
"string-quotes": "single",
"no-descending-specificity": null,
"selector-class-pattern": null,
"custom-property-pattern": null
},
"ignoreFiles": [
"node_modules/**",
".Build/**",
"**/*.min.css"
]
}
---
# TypoScript Linting Configuration - TYPO3 Extension
# Based on TYPO3 Best Practices: https://github.com/TYPO3BestPractices/tea
#
# This configuration ensures consistent TypoScript formatting and detects common issues.
sniffs:
# Indentation - enforce consistent formatting
- class: Indentation
parameters:
indentConditions: true
indentPerLevel: 2 # 2 spaces per level (TYPO3 standard)
useSpaces: true # Use spaces, not tabs
# Dead Code Detection - find unused TypoScript
- class: DeadCode
# Operator Whitespace - consistent spacing around operators
- class: OperatorWhitespace
# Repeating RValue - detect duplicate values (can be noisy, disabled by default)
- class: RepeatingRValue
disabled: true
# Duplicate Assignment - detect duplicate property assignments
- class: DuplicateAssignment
# Empty Section - warn about empty sections (can be intentional, disabled by default)
- class: EmptySection
disabled: true
# Nesting Consistency - ensure consistent object nesting
- class: NestingConsistency
parameters:
commonPathPrefixThreshold: 1
# Checkpoints for typo3-conformance skill
# Validates TYPO3 extension structure, configuration, and coding standards
# RELOCATED FROM php-modernization-skill (v1.16.0 refocus):
# PM-26 -> TC-180 (PSR-3 logging compliance)
# PM-27 -> TC-181 (PSR-14 event dispatch safety)
# PM-28 -> TC-182 (factory pattern + baseline)
# PM-33 -> TC-183 (GeneralUtility::makeInstance -> DI)
version: 1
skill_id: typo3-conformance
mechanical:
# === ext_emconf.php CHECKS ===
- id: TC-01
type: file_exists
target: ext_emconf.php
severity: error
desc: "ext_emconf.php must exist for TYPO3 extension registration"
- id: TC-02
type: contains
target: ext_emconf.php
pattern: "'title'"
severity: error
desc: "ext_emconf.php must define 'title' field"
- id: TC-03
type: contains
target: ext_emconf.php
pattern: "'description'"
severity: error
desc: "ext_emconf.php must define 'description' field"
- id: TC-04
type: contains
target: ext_emconf.php
pattern: "'version'"
severity: error
desc: "ext_emconf.php must define 'version' field"
- id: TC-05
type: contains
target: ext_emconf.php
pattern: "'constraints'"
severity: error
desc: "ext_emconf.php must define 'constraints' with TYPO3 core dependency"
- id: TC-06
type: regex
target: ext_emconf.php
pattern: "['\"]state['\"][[:space:]]*=>[[:space:]]*['\"](stable|beta|alpha|experimental|test|obsolete|excludeFromUpdates)['\"]"
severity: warning
desc: "ext_emconf.php should define valid 'state' field"
# === composer.json CHECKS ===
- id: TC-10
type: file_exists
target: composer.json
severity: error
desc: "composer.json must exist for Composer-based installation"
- id: TC-11
type: contains
target: composer.json
pattern: '"typo3-cms-extension"'
severity: error
desc: "composer.json type must be 'typo3-cms-extension'"
- id: TC-12
type: json_path
target: composer.json
pattern: '.require["typo3/cms-core"]'
severity: error
desc: "composer.json must require typo3/cms-core"
- id: TC-13
type: json_path
target: composer.json
pattern: '.autoload["psr-4"]'
severity: error
desc: "composer.json must define PSR-4 autoloading"
- id: TC-14
type: regex
target: composer.json
pattern: '"[A-Z][a-zA-Z0-9_]*\\\\[A-Z][a-zA-Z0-9_]*\\\\": "Classes/?"'
severity: error
desc: "PSR-4 autoload must map vendor namespace to Classes/ directory"
- id: TC-15
type: json_path
target: composer.json
pattern: '.extra["typo3/cms"]["extension-key"]'
severity: warning
desc: "composer.json should define extension-key in extra.typo3/cms"
- id: TC-16
type: command
pattern: "composer validate --strict 2>&1 || true"
severity: warning
desc: "composer.json should pass strict validation"
# === DIRECTORY STRUCTURE CHECKS ===
- id: TC-20
type: file_exists
target: Classes/
severity: error
desc: "Classes/ directory must exist for PHP classes"
- id: TC-21
type: file_exists
target: Configuration/
severity: warning
desc: "Configuration/ directory should exist for TCA, TypoScript, etc."
- id: TC-22
type: file_exists
target: Resources/
severity: warning
desc: "Resources/ directory should exist for templates, assets, translations"
- id: TC-23
type: file_exists
target: Resources/Private/
severity: warning
desc: "Resources/Private/ should exist for templates and partials"
- id: TC-24
type: file_exists
target: Resources/Public/
severity: info
desc: "Resources/Public/ should exist for CSS, JS, and images"
# === DEPENDENCY INJECTION CHECKS ===
- id: TC-30
type: file_exists
target: Configuration/Services.yaml
severity: warning
desc: "Services.yaml should exist for dependency injection configuration"
- id: TC-31
type: contains
target: Configuration/Services.yaml
pattern: "autowire: true"
severity: info
desc: "Services.yaml should enable autowiring"
- id: TC-32
type: contains
target: Configuration/Services.yaml
pattern: "autoconfigure: true"
severity: info
desc: "Services.yaml should enable autoconfiguration"
# === TCA CONFIGURATION CHECKS ===
- id: TC-35
type: file_exists
target: Configuration/TCA/
severity: info
desc: "Configuration/TCA/ should exist if extension defines database tables"
# === DEPRECATED API PATTERNS ===
- id: TC-40
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['TYPO3_DB']"
severity: error
desc: "Must not use deprecated TYPO3_DB global (removed in TYPO3 v10)"
- id: TC-41
type: not_contains
target: Classes/**/*.php
pattern: "GeneralUtility::makeInstance(ObjectManager"
severity: warning
desc: "Should not instantiate ObjectManager directly; use dependency injection"
- id: TC-42
type: not_contains
target: Classes/**/*.php
pattern: "ObjectManager::get("
severity: warning
desc: "Should not use ObjectManager::get(); use dependency injection"
- id: TC-43
type: not_contains
target: Classes/**/*.php
pattern: "GeneralUtility::_GP("
severity: warning
desc: "Should not use deprecated _GP(); use request object instead"
- id: TC-44
type: not_contains
target: Classes/**/*.php
pattern: "GeneralUtility::_GET("
severity: warning
desc: "Should not use deprecated _GET(); use request object instead"
- id: TC-45
type: not_contains
target: Classes/**/*.php
pattern: "GeneralUtility::_POST("
severity: warning
desc: "Should not use deprecated _POST(); use request object instead"
- id: TC-46
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['TSFE']->fe_user"
severity: warning
desc: "Should not access fe_user via TSFE global; use Context API"
- id: TC-47
type: not_contains
target: Classes/**/*.php
pattern: "extRelPath("
severity: error
desc: "Must not use deprecated extRelPath() (removed in TYPO3 v10)"
- id: TC-48
type: not_contains
target: Classes/**/*.php
pattern: "BackendUtility::getModuleUrl("
severity: error
desc: "Must not use deprecated getModuleUrl(); use UriBuilder"
# === EXTENSION REGISTRATION ===
- id: TC-50
type: file_exists
target: ext_localconf.php
severity: info
desc: "ext_localconf.php should exist for frontend/backend registration"
# ext_tables.php is OPTIONAL in TYPO3 v13+ — backend modules / AJAX routes
# are registered via Configuration/Backend/{Modules,AjaxRoutes,Routes}.php.
# Accept any of the registration entry-points (or pure-service extensions
# with no backend surface at all).
- id: TC-51
type: file_exists
target: "{ext_tables.php,Configuration/Backend/Modules.php,Configuration/Backend/AjaxRoutes.php,Configuration/Backend/Routes.php}"
severity: info
desc: "Backend modules / AJAX routes registered via either ext_tables.php (legacy) or Configuration/Backend/*.php (TYPO3 v13+ recommended). Pure-service extensions may have neither."
# === STRICT TYPES DECLARATION ===
- id: TC-07
type: regex_not
target: ext_emconf.php
pattern: '^[[:space:]]*(<\?php[[:space:]]+)?declare[[:space:]]*\([[:space:]]*strict_types'
severity: error
desc: "ext_emconf.php MUST NOT contain declare(strict_types=1) - TER upload will fail"
- id: TC-08
type: regex
target: ext_emconf.php
pattern: '\$EM_CONF\[\$_EXTKEY\]'
severity: error
desc: "ext_emconf.php must use $EM_CONF[$_EXTKEY], not a hardcoded extension key"
- id: TC-09
type: contains
target: ext_emconf.php
pattern: "'category'"
severity: warning
desc: "ext_emconf.php should define valid 'category' field"
# === CODING STANDARDS: STRICT TYPES ===
- id: TC-17
type: regex
target: Classes/**/*.php
pattern: 'declare\(strict_types=1\)'
severity: warning
desc: "All PHP files in Classes/ should declare strict_types=1"
# === COMPOSER.JSON ADDITIONAL CHECKS ===
- id: TC-18
type: json_path
target: composer.json
pattern: '.require["php"]'
severity: error
desc: "composer.json must declare PHP version constraint"
- id: TC-19
type: json_path
target: composer.json
pattern: '.license'
severity: warning
desc: "composer.json should declare license (GPL-2.0-or-later recommended)"
- id: TC-25
type: json_path
target: composer.json
pattern: '.description'
severity: warning
desc: "composer.json should have a meaningful description"
- id: TC-26
type: regex
target: composer.json
pattern: '"name"\s*:\s*"[a-z0-9-]+/[a-z0-9-]+"'
severity: error
desc: "composer.json name must follow vendor/package-name format"
- id: TC-27
type: not_contains
target: composer.json
pattern: '"typo3-ter/'
severity: warning
desc: "composer.json should not contain deprecated typo3-ter replace entries"
# === ADDITIONAL PROHIBITED PATTERNS ===
- id: TC-36
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['TYPO3_CONF_VARS']"
severity: warning
desc: "Should not access TYPO3_CONF_VARS via $GLOBALS; use ExtensionConfiguration"
- id: TC-37
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['BE_USER']"
severity: warning
desc: "Should not access BE_USER via $GLOBALS; use Context API"
- id: TC-38
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['LANG']"
severity: warning
desc: "Should not access LANG via $GLOBALS; use LanguageServiceFactory"
- id: TC-39
type: not_contains
target: Classes/**/*.php
pattern: "$GLOBALS['TCA']"
severity: warning
desc: "Should not access TCA via $GLOBALS; use TcaSchemaFactory (v14+) or inject TCA via DI"
# === BACKEND MODULE MODERNIZATION (v13+) ===
- id: TC-52
type: command
pattern: "! test -d Resources/Private/Templates/ || ! grep -rq '<script' Resources/Private/Templates/ 2>/dev/null"
severity: warning
desc: "Templates should not contain inline <script> tags; use ES6 modules"
- id: TC-53
type: file_exists
target: Configuration/Icons.php
severity: info
scope: backend
desc: "Configuration/Icons.php should exist for icon registration (v12+)"
# Configuration/Sets/ is REQUIRED for sitepackage extensions in TYPO3 v13+
# (Site Sets) but is N/A for service/provider/library extensions. Detect
# sitepackage by the presence of typical sitepackage markers (page
# templates, page.tsconfig, Yaml/Site/). If none of those exist → skip;
# otherwise require Configuration/Sets/.
- id: TC-54
type: command
pattern: 'if [ -e Configuration/page.tsconfig ] || [ -d Configuration/Yaml/Site ] || [ -d Resources/Private/Templates/Page ] || [ -d Resources/Private/Layouts/Page ]; then [ -d Configuration/Sets ]; else exit 0; fi'
severity: info
desc: "Configuration/Sets/ required for sitepackage extensions in TYPO3 v13+ (detected via page templates / page.tsconfig / Yaml/Site/). Skipped for service/provider extensions."
# === CI TOOLING CENTRALIZATION ===
- id: TC-90
type: json_path
target: composer.json
pattern: '.["require-dev"]["netresearch/typo3-ci-workflows"]'
severity: warning
desc: "require-dev should use netresearch/typo3-ci-workflows for centralized CI tooling"
- id: TC-91
type: not_contains
target: composer.json
pattern: '"friendsofphp/php-cs-fixer"'
severity: info
desc: "require-dev should not individually list php-cs-fixer (provided by typo3-ci-workflows)"
- id: TC-92
type: file_exists
target: Build/Scripts/runTests.sh
severity: warning
desc: "Build/Scripts/runTests.sh should exist for local test execution"
- id: TC-93
type: regex
target: "{phpstan.neon,Build/phpstan.neon,Build/phpstan/phpstan.neon,phpstan.neon.dist}"
pattern: 'typo3-ci-workflows'
severity: info
desc: "PHPStan config should include shared config from typo3-ci-workflows"
# === TCA v13 PATTERN CHECKS ===
- id: TC-100
type: regex_not
target: Configuration/TCA/**/*.php
pattern: "eval.*trim"
severity: warning
desc: "TCA files should not contain eval=trim; use trim option in fieldControl instead (v13+)"
- id: TC-101
type: regex_not
target: Configuration/TCA/**/*.php
pattern: "l10n_parent.*type.*group"
severity: error
desc: "TCA translation l10n_parent must not use type=group; use type=select with renderType=selectSingle"
- id: TC-102
type: regex_not
target: Configuration/TCA/**/*.php
pattern: "(prependAtCopy|hideAtCopy)"
severity: warning
desc: "TCA should not contain prependAtCopy or hideAtCopy; removed in TYPO3 v13"
- id: TC-103
type: command
command: |
found=$(find Configuration/TCA/ -name '*.php' -exec grep -lP 'foreign_table' {} + 2>/dev/null)
[ -z "$found" ] && exit 0
missing=$(echo "$found" | while IFS= read -r f; do
grep -LP 'default' "$f" 2>/dev/null
done)
if [ -n "$missing" ]; then
echo "TCA files with foreign_table but no default value: $missing"
exit 1
fi
exit 0
severity: warning
desc: "TCA select fields with foreign_table should define a default value to avoid NULL issues"
# === DI / ARCHITECTURE CHECKS ===
- id: TC-110
type: command
pattern: "set -o pipefail; ! grep -rn 'GeneralUtility::makeInstance' Classes/ 2>/dev/null | grep -v 'Classes/Task/' | grep -v 'Classes/Scheduler/' | grep -v 'Typo3QuerySettings' | grep -v 'ConnectionPool' | grep -v 'QueryBuilder' | grep -v 'FlashMessage' | grep -q ."
severity: warning
desc: "Classes/ should use dependency injection instead of GeneralUtility::makeInstance for services (excludes Scheduler tasks, ConnectionPool, QueryBuilder, FlashMessage)"
- id: TC-111
type: regex_not
target: Classes/**/*.php
pattern: "PersistenceManager[^I]"
severity: warning
desc: "Should use PersistenceManagerInterface in type hints, not concrete PersistenceManager class"
- id: TC-112
type: not_contains
target: Classes/**/*.php
pattern: "GeneralUtility::makeInstance(ExtensionConfiguration"
severity: warning
desc: "Should inject ExtensionConfiguration via constructor DI instead of using GeneralUtility::makeInstance"
# === SECURITY PATTERN CHECKS ===
- id: TC-120
type: command
command: |
found=$(find Classes/ -name '*.php' -exec grep -lP '(shell_exec|\bexec\s*\(|\bsystem\s*\(|passthru\s*\()' {} + 2>/dev/null)
if [ -n "$found" ]; then
echo "Prohibited function usage found in: $found"
exit 1
fi
exit 0
severity: error
desc: "Must not use shell_exec, exec, system, or passthru; use CommandUtility or Symfony Process"
- id: TC-121
type: not_contains
target: Classes/**/*.php
pattern: "md5(uniqid"
severity: error
desc: "Must not use md5(uniqid()); use random_bytes() or GeneralUtility::makeInstance(Random::class) — md5(uniqid()) is cryptographically weak and a security vulnerability"
- id: TC-122
type: regex_not
target: Classes/**/*.php
pattern: "\\bserialize\\s*\\("
severity: error
desc: "Must not use serialize(); prefer json_encode/json_decode — serialize() enables object injection attacks"
- id: TC-122b
type: regex_not
target: Classes/**/*.php
pattern: "\\bunserialize\\s*\\("
severity: error
desc: "Must not use unserialize(); use json_decode instead — unserialize() without allowed_classes is an object injection vulnerability"
- id: TC-123
type: command
pattern: "grep -rn '{[a-zA-Z]\\+}\\|{[a-zA-Z]\\+\\.' Resources/Private/Templates/ 2>/dev/null | grep -v 'f:format.raw\\|f:format.html\\|f:uri\\|f:link\\|f:translate\\|f:image\\|f:if\\|f:for\\|f:render\\|f:section\\|f:layout\\|f:comment\\|f:count\\|f:format.htmlspecialchars\\|f:security\\|f:be\\.' | head -5 && echo 'INFO: Check that dynamic Fluid output uses f:format.htmlspecialchars where appropriate' || true"
severity: warning
desc: "Fluid templates should use f:format.htmlspecialchars on dynamic output to prevent XSS"
# === TEMPLATE / BOOTSTRAP 5 CHECKS ===
- id: TC-130
type: regex_not
target: Resources/Private/**/*.html
pattern: "(form-inline|form-row|btn-default|form-group)"
severity: warning
desc: "Templates should not use Bootstrap 4 classes (form-inline, form-row, btn-default); migrate to Bootstrap 5"
# === CI CONFIGURATION CHECKS ===
- id: TC-140
type: command
command: |
# Only fail if push trigger exists WITHOUT branch restrictions
grep -A2 '^on:' .github/workflows/ci.yml 2>/dev/null | grep -q 'push:' || exit 0
grep -A5 'push:' .github/workflows/ci.yml 2>/dev/null | grep -q 'branches' && exit 0
echo 'ci.yml has unrestricted push trigger — should restrict to specific branches'
exit 1
severity: warning
desc: "CI workflow push trigger should be restricted to specific branches, not bare push:"
- id: TC-141
type: json_path
target: composer.json
pattern: '.["require-dev"]["netresearch/typo3-ci-workflows"]'
severity: info
desc: "require-dev should reference netresearch/typo3-ci-workflows for centralized CI tooling"
# === PHPSTAN BASELINE CHECKS ===
- id: TC-150
type: command
pattern: "if [ -f phpstan-baseline.neon ]; then count=$(grep -c 'message:' phpstan-baseline.neon 2>/dev/null || echo 0); if [ \"$count\" -gt 10 ]; then echo \"WARN: PHPStan baseline has $count suppressed errors (should trend toward 0)\" && exit 1; fi; elif [ -f Build/phpstan-baseline.neon ]; then count=$(grep -c 'message:' Build/phpstan-baseline.neon 2>/dev/null || echo 0); if [ \"$count\" -gt 10 ]; then echo \"WARN: PHPStan baseline has $count suppressed errors (should trend toward 0)\" && exit 1; fi; fi; exit 0"
severity: warning
desc: "PHPStan baseline should trend toward zero suppressed errors (>10 indicates significant technical debt)"
# === TCA SEARCHFIELDS AND SORTBY CHECKS ===
- id: TC-151
type: command
pattern: "find Configuration/TCA/ -name '*.php' 2>/dev/null | xargs grep -lP \"'ctrl'\" 2>/dev/null | while read f; do if ! grep -qP \"'searchFields'\" \"$f\" 2>/dev/null; then echo \"INFO: $f has no ctrl.searchFields (v12/v13: define it for backend search; v14: removed, all suitable fields searchable by default)\"; fi; done; exit 0"
severity: info
desc: "TCA backend search config: on v12/v13 define ctrl.searchFields; ctrl.searchFields was REMOVED in v14.0 (#106972) — all suitable fields are searchable by default and a field is excluded with per-column 'searchable' => false. Do not require searchFields for v14."
- id: TC-152
type: command
pattern: "find Configuration/TCA/ -name '*.php' 2>/dev/null | xargs grep -lP \"['\\\"]sortby['\\\"]\" 2>/dev/null | while read f; do if ! grep -qP \"['\\\"]default_sortby['\\\"]\" \"$f\" 2>/dev/null; then echo \"WARN: $f has sortby but no default_sortby\" && exit 1; fi; done"
severity: warning
desc: "TCA tables with sortby should also define default_sortby for consistent ordering"
# === DI INTERFACE ALIAS CHECKS ===
- id: TC-153
type: command
pattern: "interfaces=$(grep -rhoP '[A-Z][a-zA-Z0-9_]*Interface' Classes/ 2>/dev/null | sort -u); if [ -n \"$interfaces\" ] && [ -f Configuration/Services.yaml ]; then missing=0; for iface in $interfaces; do case $iface in ServerRequestInterface|ResponseInterface|ContainerInterface|LoggerInterface|EventDispatcherInterface|CacheInterface|RequestFactoryInterface|UriFactoryInterface|StreamFactoryInterface) continue ;; esac; if ! grep -q \"$iface\" Configuration/Services.yaml 2>/dev/null; then echo \"WARN: Interface $iface used in Classes/ but not aliased in Services.yaml\"; missing=1; fi; done; if [ \"$missing\" -eq 1 ]; then exit 1; fi; fi; exit 0"
severity: warning
desc: "Interfaces used for constructor injection should have explicit aliases in Services.yaml"
# === CACHE DOUBLE-LOOKUP ANTI-PATTERN ===
- id: TC-154
type: command
pattern: "[ -d Classes/ ] || exit 0; set -o pipefail; ! grep -rnP '->has\\(' Classes/ 2>/dev/null | sed -E 's/^[^:]+:[0-9]+://' | grep -iq cache"
severity: warning
desc: "Cache get should not be preceded by has() -- use get() directly and check for false (path-prefix stripped to avoid false-positives from Cache/ directory names)"
# === REPOSITORY QUERY PROPERTY NAME CHECK ===
# Test PHP source under Classes/Domain/Repository/ for snake_case names
# passed to ->equals() / ->like() / ->contains(). Filenames are stripped
# before the snake_case check (sed) so directory names like Cache_Foo/
# don't generate false positives.
- id: TC-155
type: command
pattern: "[ -d Classes/Domain/Repository/ ] || exit 0; set -o pipefail; ! grep -rn '\\->equals(' Classes/Domain/Repository/ 2>/dev/null | sed -E 's/^[^:]+:[0-9]+://' | grep -Eq '_[a-z]'"
severity: error
desc: "Extbase repository queries should use model property names (camelCase), not database column names (snake_case)"
# === XLIFF COMPLETENESS CHECK ===
- id: TC-156
type: command
pattern: "lll_refs=$(grep -rhoP 'LLL:EXT:[^/]+/Resources/Private/Language/[^:]+:[a-zA-Z0-9_.]+' Configuration/ Classes/ Resources/Private/Templates/ 2>/dev/null | sed 's/.*://' | sort -u); if [ -n \"$lll_refs\" ]; then missing=0; for key in $lll_refs; do if ! grep -rq \"id=\\\"$key\\\"\" Resources/Private/Language/ 2>/dev/null; then echo \"MISSING XLIFF key: $key\"; missing=$((missing+1)); fi; done; if [ $missing -gt 0 ]; then echo \"FAIL: $missing LLL references have no XLIFF trans-unit\" && exit 1; fi; fi; exit 0"
severity: error
desc: "All LLL: references in TCA, classes, and templates must have corresponding XLIFF trans-unit entries"
# === DEPRECATED CONSTANTS WITH ENUM REPLACEMENTS ===
# Walk PHP files: detect @deprecated docblocks followed by a `const`
# declaration (typical pattern — @deprecated lives in the docblock above
# the const, not on the same line). Single-line greps would miss this.
- id: TC-157
type: command
pattern: "[ -d Classes/ ] || exit 0; set -o pipefail; if find Classes/ -type f -name '*.php' 2>/dev/null | xargs awk 'BEGIN{in_doc=0; doc_deprecated=0; pending_deprecated=0} /^[[:space:]]*\\/\\*\\*/{in_doc=1; doc_deprecated=0} {if (in_doc && /@deprecated/) doc_deprecated=1} in_doc && /\\*\\//{in_doc=0; if (doc_deprecated) pending_deprecated=1; next} pending_deprecated && /^[[:space:]]*(public|protected|private)?[[:space:]]*const[[:space:]]+/ {found=1; exit} pending_deprecated && /^[[:space:]]*(\\/\\*\\*|#|\\/\\/|$)/ {next} pending_deprecated {pending_deprecated=0} END{exit found ? 0 : 1}' 2>/dev/null && grep -rlqP '\\benum\\b' Classes/ 2>/dev/null; then exit 1; else exit 0; fi"
severity: warning
desc: "Deprecated string constants (in @deprecated docblock) should not coexist with enum replacements in active code paths"
# === EXCELLENCE INDICATORS (bonus) ===
- id: TC-58
type: file_exists
target: .gitattributes
severity: info
desc: ".gitattributes should exist with export-ignore for smaller TER packages"
- id: TC-59
type: file_exists
target: Makefile
severity: info
desc: "Makefile should exist for task automation"
# === TER PUBLISHING CHECKS ===
- id: TC-55
type: json_path
target: composer.json
pattern: '.support.issues'
severity: warning
desc: "composer.json should define support.issues URL for bug tracker link"
- id: TC-56
type: json_path
target: composer.json
pattern: '.support.source'
severity: warning
desc: "composer.json should define support.source URL for repository link"
- id: TC-57
type: json_path
target: composer.json
pattern: '.homepage'
severity: warning
desc: "composer.json should define homepage URL"
# === BOOTSTRAP 5 MIGRATION CHECKS ===
- id: TC-65
type: regex_not
target: "Resources/Private/**/*.html"
pattern: 'data-dismiss=|data-toggle="(collapse|modal|tab|dropdown|tooltip|popover)"|data-target=|data-ride=|data-slide='
severity: warning
desc: "Legacy Bootstrap 4 data attributes detected. Migrate to Bootstrap 5: data-dismiss→data-bs-dismiss, data-toggle→data-bs-toggle, data-target→data-bs-target, data-ride→data-bs-ride, data-slide→data-bs-slide"
- id: TC-66
type: regex_not
target: "Resources/Private/**/*.html"
pattern: '\bclass="[^"]*\b(btn-default|ml-(\d+|auto)|mr-(\d+|auto)|pl-(\d+|auto)|pr-(\d+|auto)|float-left|float-right|font-weight-bold|font-weight-normal|font-italic)\b'
severity: warning
desc: "Legacy Bootstrap 4 CSS classes detected. Migrate: btn-default→btn-secondary, ml-*→ms-*, mr-*→me-*, pl-*→ps-*, pr-*→pe-*, float-left→float-start, float-right→float-end"
# === TEMPLATE QUALITY CHECKS ===
- id: TC-67
type: regex_not
target: "Resources/Private/**/*.html"
pattern: 'includeJavaScriptFiles|includeCssFiles'
severity: error
desc: "Wrong f:be.pageRenderer attribute name. Use includeJsFiles (NOT includeJavaScriptFiles) and includeCssLibs (NOT includeCssFiles). Wrong names silently fail to load assets"
- id: TC-68
type: regex_not
target: "Resources/Private/**/*.html"
pattern: '<script(?![^>]*\bsrc=)[^>]*>'
severity: error
desc: "Inline <script> tags violate CSP. Move JavaScript to external files loaded via f:be.pageRenderer includeJsFiles or AssetCollector"
# === CODECOV PATCH TARGET CHECK ===
- id: TC-160
type: command
command: "if [ ! -f codecov.yml ]; then exit 0; fi; target=$(sed -n '/patch:/,/target:/{ s/.*target:\\s*//p; }' codecov.yml 2>/dev/null | head -1 || echo auto); case \"$target\" in auto|'') exit 0 ;; 100%) echo \"WARN: codecov patch target is 100% — use 80% or auto when only unit test coverage is uploaded\" && exit 1 ;; *) exit 0 ;; esac"
severity: warning
desc: "codecov patch target should not be 100% — use 80% or auto when only unit test coverage is uploaded"
tags: [ci, codecov, coverage]
# === PHPSTAN LEVEL 10 CHECK ===
- id: TC-176
type: command
command: "if ! grep -q 'typo3-ci-workflows' composer.json 2>/dev/null; then exit 0; fi; for f in Build/phpstan.neon phpstan.neon; do if [ -f \"$f\" ] && grep -qP 'level:\\s*(10|max)' \"$f\" 2>/dev/null; then exit 0; fi; done; echo 'WARN: PHPStan should use level 10 (max) for extensions using typo3-ci-workflows shared config' && exit 1"
severity: warning
desc: "PHPStan should use level 10 (max) for extensions using typo3-ci-workflows shared config"
tags: [phpstan, ci, static-analysis]
# === PHPSTAN EMPTY BASELINE (ASPIRATIONAL) ===
- id: TC-177
type: regex_not
target: "{Build/phpstan-baseline.neon,phpstan-baseline.neon}"
pattern: '^\s+-\s*$\n\s+message:'
severity: info
desc: "PHPStan baseline should be empty — zero suppressed errors is the target"
tags: [phpstan, baseline, aspirational]
# Note: TC-177 is aspirational; TC-150 catches excessive baselines
llm_reviews:
# === CODE QUALITY REVIEWS ===
- id: TC-60
domain: code-quality
prompt: |
Review the Classes/ directory for TYPO3 coding standards compliance:
1. Check for proper namespace declarations matching PSR-4 autoload
2. Verify constructor injection is used for dependencies
3. Check that controllers extend ActionController or appropriate base class
4. Verify services are stateless and do not hold mutable state
5. Check for proper use of TYPO3 attributes (#[AsController], etc.)
severity: warning
desc: "Classes should follow TYPO3 coding standards"
- id: TC-61
domain: code-quality
prompt: |
Check for modern TYPO3 API usage:
1. Verify PSR-7 Request/Response is used in controllers
2. Check for proper use of Site and SiteLanguage objects
3. Verify TypoScriptFrontendController is not accessed directly
4. Check that Context API is used for user/workspace info
5. Verify FlexForm handling uses modern approaches
severity: warning
desc: "Extension should use modern TYPO3 APIs"
- id: TC-62
domain: code-quality
prompt: |
Review TCA configuration in Configuration/TCA/:
1. Check for proper ctrl configuration
2. Verify columns use modern renderType instead of deprecated types
3. Check showitem syntax is correct
4. Verify locallang references exist
5. Check for proper icon registration
severity: info
desc: "TCA configuration should follow current standards"
# === TRANSLATION REVIEWS ===
- id: TC-63
name: Translation keys complete for all features
type: llm_review
severity: info
domain: i18n
prompt: |
Verify that all user-visible strings in the extension have
corresponding XLIFF translation keys. Check:
- Controller error messages (should use LLL references or trait)
- Template labels and buttons
- TCA column labels
- Module labels
- Configuration labels in ext_conf_template.txt
tags: [xliff, translation, i18n, completeness]
# === DOCUMENTATION REVIEWS ===
- id: TC-64
domain: documentation
prompt: |
Check extension documentation completeness:
1. Verify README.md or Documentation/ exists
2. Check for installation instructions specific to TYPO3
3. Verify configuration options are documented
4. Check if TypoScript/FlexForm options are explained
5. Verify version compatibility is clearly stated
severity: info
desc: "Extension documentation should be complete"
# === PUBLIC LISTING VERIFICATION ===
- id: TC-80
domain: publishing
prompt: |
Verify public listing presence and completeness for the extension.
Extract the composer package name from composer.json and the extension
key from ext_emconf.php, then check:
1. TER Listing (extensions.typo3.org/extension/{extension_key}):
- Extension page exists and is accessible
- Sidebar links are populated (Extension Manual, Found an Issue,
Code Insights, Packagist.org)
- If links are missing, they must be set via:
TYPO3_API_TOKEN=... tailor ter:update \
--composer="vendor/package-name" \
--manual="https://github.com/..." \
--issues="https://github.com/.../issues" \
--repository="https://github.com/..." \
{extension_key}
- Version shown matches latest release
2. Packagist Listing (packagist.org/packages/{vendor/name}):
- Package exists and is findable
- Description is meaningful (not empty)
- License is set
- Latest version is published
3. Documentation (docs.typo3.org):
- If Documentation/ directory with guides.xml exists, check if
rendered documentation is available at docs.typo3.org
- Alternatively, README or external docs linked from TER
Report missing or incomplete listings as actionable items.
severity: warning
desc: "Extension should have complete TER, Packagist, and documentation listings"
- id: TC-81
domain: publishing
prompt: |
Check the TER publishing workflow for metadata setup completeness:
1. Verify .github/workflows/ contains a TER publish workflow
2. Check if the workflow includes a tailor ter:update step that sets:
- --composer (package name for Packagist link)
- --manual (documentation/repository link)
- --issues (issue tracker link)
- --repository (source code link)
3. If ter:update is missing from the workflow, recommend adding it
as a post-publish step or as a separate one-time setup step
The ter:update command sets the sidebar links on extensions.typo3.org.
Without it, the TER page shows no links to documentation, issues,
source code, or Packagist - making the extension appear incomplete.
This is a MANDATORY step for initial TER setup. The links persist
across releases, so it only needs to run once (or when URLs change).
severity: warning
desc: "TER publish workflow should include metadata setup via tailor ter:update"
# === CONFIGURATION SYNC REVIEWS ===
- id: TC-69
domain: configuration
prompt: |
Compare TYPO3 version constraints between ext_emconf.php and composer.json:
1. Check that 'typo3' constraint in ext_emconf.php constraints.depends matches composer.json require.typo3/cms-core version range
2. Check that PHP version constraint matches between both files
3. Flag any mismatch that would cause installation failures
severity: error
desc: "ext_emconf.php and composer.json version constraints must be synchronized to prevent installation failures"
# === DEPENDENCY INJECTION REVIEWS ===
- id: TC-70
domain: architecture
prompt: |
Review Services.yaml for proper dependency injection patterns:
1. Check factory wiring uses correct syntax: factory: ['@ServiceClass', 'methodName'] with arguments
2. Check interface alias bindings map interfaces to concrete implementations
3. Check EventDispatcherInterface is properly autowired for event-dispatching services
4. Flag any manual service definition that could use autowiring instead
severity: warning
desc: "Services.yaml should use proper factory wiring, interface bindings, and EventDispatcher autowiring patterns"
# === SECURITY REVIEWS ===
- id: TC-71
domain: security
prompt: |
Review custom ViewHelper classes for XSS prevention:
1. Check that ALL dynamic attribute values in tag() or manual HTML construction use htmlspecialchars() with ENT_QUOTES
2. Check that render() methods don't concatenate user input into HTML without escaping
3. Check that TagBuilder is used instead of manual string concatenation where possible
4. Flag any unescaped attribute value as critical XSS risk
severity: error
desc: "Custom ViewHelper tag attributes must use htmlspecialchars(value, ENT_QUOTES) to prevent XSS"
# === TRANSLATION HYGIENE REVIEWS ===
- id: TC-72
domain: quality
prompt: |
Check for unused XLIFF translation keys:
1. List all trans-unit id values in Resources/Private/Language/*.xlf files
2. Search for each key in Classes/**/*.php (LLL: references) and Resources/Private/Templates/**/*.html (f:translate references)
3. Flag keys that appear in XLIFF but are never referenced in code or templates
4. Also flag translation keys referenced in code but missing from XLIFF
severity: info
desc: "Unused XLIFF translation keys should be removed to maintain translation hygiene"
# === ASSESSMENT AUDIT REVIEWS (from comprehensive audit session) ===
- id: TC-73
domain: architecture
prompt: |
Review dependency injection completeness:
1. List all constructor parameters with interface type-hints in Classes/
2. Check Configuration/Services.yaml for explicit alias bindings for each interface
3. PSR interfaces (Psr\*) and core TYPO3 interfaces (LoggerInterface, EventDispatcherInterface)
are autowired and don't need aliases -- skip these
4. Custom or extension-specific interfaces MUST have explicit aliases in Services.yaml
5. Flag any interface used in constructor injection that lacks a Services.yaml alias
severity: error
desc: "All custom interfaces used for constructor injection must have explicit aliases in Services.yaml"
- id: TC-74
domain: i18n
prompt: |
Cross-reference all LLL: translation key usage with XLIFF definitions:
1. Collect all LLL:EXT:*/Resources/Private/Language/*:key references from TCA files,
PHP classes, and Fluid templates
2. Parse all .xlf files in Resources/Private/Language/ for trans-unit id values
3. Report keys referenced in code but missing from XLIFF (broken translations)
4. Report keys defined in XLIFF but never referenced (dead translations)
5. Check that default language XLIFF has all keys used in the extension
severity: error
desc: "All LLL: references must have corresponding XLIFF entries; orphaned XLIFF keys should be removed"
- id: TC-75
domain: code-quality
prompt: |
Check for deprecated constants that coexist with enum replacements:
1. Find all class constants marked @deprecated in Classes/
2. Check if enum classes exist that provide replacement values
3. Search for references to the deprecated constants in active (non-deprecated) code paths
4. Flag any active code that still uses deprecated constants when enum equivalents exist
5. Recommend migration path from constants to enums
severity: warning
desc: "Deprecated constants should not be referenced in active code when enum replacements exist"
- id: TC-76
domain: performance
prompt: |
Check for cache double-lookup anti-pattern:
1. Find all cache interaction code in Classes/ (FrontendInterface, CacheManager)
2. Identify patterns where cache->has() is called before cache->get() in the same method
3. This is an anti-pattern because it requires two cache lookups instead of one
4. The correct pattern is: $result = $cache->get($key); if ($result === false) { ... }
5. Flag all instances of has()-then-get() pattern with suggested fix
severity: warning
desc: "Cache has()+get() double-lookup should be replaced with get() and false check"
# === CODING STANDARDS REVIEWS ===
- id: TC-95
domain: coding-standards
prompt: |
Check that ALL PHP files have a copyright/license header comment block.
This includes ext_localconf.php, ext_tables.php, Configuration/**/*.php,
and Build/*.php. Exception: ext_emconf.php (TER cannot parse strict_types).
severity: warning
desc: "Copyright headers on all PHP files including configuration"
# === ASSESSMENT-DERIVED CHECKPOINTS (quality audit findings) ===
# --- Mechanical checks from assessment findings ---
# Note: TC-160 (makeInstance) removed — covered by existing TC-110
- id: TC-161
name: datahandler-hook-completeness
type: command
pattern: "if grep -qP 'processDatamapClass' ext_localconf.php 2>/dev/null; then if ! grep -qP 'processCmdmapClass' ext_localconf.php 2>/dev/null; then echo 'WARNING: processDatamapClass registered but processCmdmapClass missing -- delete/copy operations will not be handled'; exit 1; fi; fi; exit 0"
severity: warning
desc: "If processDatamapClass is registered, processCmdmapClass should also be registered for delete/copy handling"
tags: [hooks, datahandler, completeness, assessment]
- id: TC-162
name: changelog-completeness
type: command
command: |
[ ! -f CHANGELOG.md ] && exit 0
missing=0
for tag in $(git tag --sort=-v:refname 2>/dev/null | head -20); do
version=$(echo "$tag" | sed 's/^v//')
if ! grep -qF "## [${version}]" CHANGELOG.md 2>/dev/null; then
echo "WARNING: No CHANGELOG.md entry for tag $tag"
missing=$((missing+1))
fi
done
if [ "$missing" -gt 0 ]; then
echo "WARNING: $missing tagged versions missing from CHANGELOG.md"
exit 1
fi
exit 0
severity: warning
desc: "CHANGELOG.md should have entries for all tagged versions (compare git tags vs changelog headings)"
tags: [documentation, changelog, completeness, assessment]
# --- LLM review checks from assessment findings ---
- id: TC-96
name: version-consistency
domain: configuration
prompt: |
Compare the version declared in guides.xml (if it exists in Documentation/)
with the version in ext_emconf.php:
1. Extract the <release> or <version> value from Documentation/guides.xml
2. Extract the 'version' value from ext_emconf.php
3. These MUST match exactly
4. Also check composer.json version field if present
5. Flag any mismatch as it causes documentation rendering to show wrong version
severity: error
desc: "guides.xml version must match ext_emconf.php version to avoid documentation version mismatch"
tags: [documentation, version, sync, assessment]
- id: TC-97
name: n-plus-one-queries
domain: performance
prompt: |
Detect N+1 query patterns in Classes/:
1. Look for loop constructs (foreach, for, while) that contain calls to
repository methods (findBy*, findAll, findOneBy*) or adapter/client
methods (retrieve, fetch, get, list, query) inside the loop body
2. The classic pattern is: list items in outer call, then retrieve details
for each item inside a loop (list-then-retrieve anti-pattern)
3. Flag any repository or service method called inside a loop that could
be batched into a single query with an IN-clause or bulk fetch
4. Check for DataMapper/QueryBuilder calls inside foreach loops
5. Suggest batching strategy for each finding
severity: warning
desc: "Detect loop-inside-loop patterns where inner loop calls repository/adapter methods (N+1 query anti-pattern)"
tags: [performance, queries, n-plus-one, assessment]
- id: TC-98
name: interface-implementation-sync
domain: architecture
prompt: |
Check that interface and implementation classes are synchronized:
1. Find all interfaces defined in Classes/ (files ending in Interface.php)
2. For each interface, find the implementing class(es)
3. Compare public methods: all public methods on the implementation that
are part of the domain contract should be declared in the interface
4. Flag public methods on implementations that are missing from their interface
(excluding __construct, framework-required methods, and getter/setter pairs
for internal state)
5. This ensures the interface remains the complete contract for DI consumers
severity: warning
desc: "All public domain methods on implementation classes should be declared in their interface"
tags: [architecture, interfaces, di, assessment]
- id: TC-99
name: hardcoded-template-strings
domain: i18n
prompt: |
Check Fluid templates (.html) in Resources/Private/ for hardcoded English strings:
1. Scan all .html files for quoted string attributes containing 3+ English words
(e.g. title="Click here to submit", placeholder="Enter your name")
2. Check for bare text content between HTML tags that is clearly English prose
(not Fluid expressions, not HTML entities, not CSS class names)
3. These should use f:translate viewhelper or LLL: references instead
4. Exclude: Fluid namespace declarations, xmlns attributes, CSS class names,
JavaScript code, and technical identifiers
5. Flag each finding with the file, line, and suggested f:translate replacement
severity: warning
desc: "Fluid templates should use f:translate instead of hardcoded English strings (check quoted strings >3 words in attributes)"
tags: [i18n, templates, hardcoded-strings, assessment]
- id: TC-100b
name: makefile-agents-consistency
domain: documentation
prompt: |
Cross-reference AGENTS.md and Makefile for consistency:
1. Parse AGENTS.md for any documented make targets or commands referencing
'make <target>' patterns
2. Parse Makefile for actually defined targets
3. Flag targets mentioned in AGENTS.md that do not exist in Makefile
4. Flag important Makefile targets that are not documented in AGENTS.md
5. This ensures agent instructions match available automation
severity: info
desc: "Targets documented in AGENTS.md should exist in Makefile and vice versa"
tags: [documentation, consistency, makefile, agents, assessment]
- id: TC-100c
name: upgrade-wizard-for-schema
domain: architecture
prompt: |
Check if database schema changes have corresponding upgrade wizards:
1. Parse ext_tables.sql for column definitions
2. Check git history or compare with previous tagged versions for NEW columns
(columns not present in earlier versions)
3. If new columns are added that require data migration (not just nullable
additions), verify an UpgradeWizardInterface implementation exists in
Classes/Updates/ or Classes/Upgrade/
4. New required columns (NOT NULL without DEFAULT), renamed columns, or
type changes especially need upgrade wizards
5. Flag schema changes that likely need migration but lack an upgrade wizard
severity: warning
desc: "New ext_tables.sql columns requiring data migration should have an UpgradeWizardInterface implementation"
tags: [architecture, upgrade, schema, assessment]
- id: TC-100d
name: ext-conf-template-alignment
domain: configuration
prompt: |
Verify ext_conf_template.txt options match implemented functionality:
1. Parse all option keys from ext_conf_template.txt (or system.yaml
extension configuration)
2. Search Classes/ for references to each configuration option via
ExtensionConfiguration->get() or $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']
3. Flag options defined in ext_conf_template.txt but never read in code
(phantom options for unimplemented features)
4. Flag options read in code but not defined in ext_conf_template.txt
(missing configuration UI)
5. Check that option descriptions match actual behavior
severity: warning
desc: "Options in ext_conf_template.txt should match implemented functionality (no phantom options)"
tags: [configuration, alignment, assessment]
# === MULTI-VERSION DEPENDENCY COMPATIBILITY CHECKS ===
- id: TC-170
name: multi-version-direct-api-usage
type: command
command: |
# Check if composer.json has multi-major-version constraints (^X || ^Y)
multi_deps=$(grep -oP '"[^"]+"\s*:\s*"[^"]*\|\|[^"]*"' composer.json 2>/dev/null | grep -vP '^"php"\s*:|^"typo3/cms-[^"]*"\s*:')
[ -z "$multi_deps" ] && exit 0
# For each multi-version dep, check if version-specific classes are used outside Adapter/
violations=0
while IFS= read -r dep; do
pkg=$(echo "$dep" | grep -oP '"[^"]+"' | head -1 | tr -d '"')
# Use the vendor segment (before the first /) as a namespace prefix and search case-insensitively
vendor=$(echo "$pkg" | cut -d'/' -f1)
vendor_ns_prefix=$(echo "$vendor" | sed 's|/|\\\\|g')
if grep -rinP "use\s+${vendor_ns_prefix}\\\\\\\\" Classes/ 2>/dev/null | grep -vP 'Adapter/|Factory/' | head -3; then
echo "WARN: Direct use of multi-version dependency $pkg outside Adapter/ classes"
violations=1
fi
done <<<"$multi_deps"
[ "$violations" -gt 0 ] && exit 1
exit 0
severity: warning
desc: "Multi-version dependencies (^X || ^Y) should be accessed through adapter interfaces, not used directly in business logic"
tags: [architecture, multi-version, adapter, dependency]
- id: TC-171
name: multi-version-adapter-interface-exists
type: command
command: |
multi_deps=$(grep -oP '"[^"]+"\s*:\s*"[^"]*\|\|[^"]*"' composer.json 2>/dev/null | grep -vP '^"php"\s*:|^"typo3/cms-[^"]*"\s*:')
[ -z "$multi_deps" ] && exit 0
# Only require adapters when TC-170 detected direct API usage (version-specific code)
# Without direct API usage evidence, the dependency APIs may be identical across versions
if [ -d Classes/Adapter ] || [ -d Classes/Service ]; then
ifaces=$(find Classes/Adapter Classes/Service -name '*Interface.php' 2>/dev/null)
if [ -z "$ifaces" ]; then
echo "HINT: Multi-version dependencies found but no adapter interfaces in Classes/Adapter/ or Classes/Service/ — consider adding adapters if APIs differ between versions"
fi
else
echo "HINT: Multi-version dependencies found but no Classes/Adapter/ directory — consider adding adapters if APIs differ between versions"
fi
exit 0
severity: info
desc: "Extensions with multi-version dependencies may benefit from adapter interfaces when APIs differ between major versions"
tags: [architecture, multi-version, adapter, interface]
- id: TC-172
name: adapter-interface-wired-in-services-yaml
type: command
command: |
# Find adapter interfaces in Classes/
ifaces=$(find Classes/Adapter Classes/Service -name '*Interface.php' 2>/dev/null | sed 's|^Classes/||')
[ -z "$ifaces" ] && exit 0
[ ! -f Configuration/Services.yaml ] && echo "FAIL: Adapter interfaces exist but no Services.yaml" && exit 1
missing=0
# Iterate safely over newline-delimited interfaces and search by short interface name
while IFS= read -r iface; do
# Normalise any backslashes to forward slashes, then extract the short class name without extension
iface_path=$(printf '%s\n' "$iface" | sed 's|\\|/|g')
classname=${iface_path##*/}
classname=${classname%.php}
if ! grep -q "$classname" Configuration/Services.yaml 2>/dev/null; then
echo "FAIL: Interface $iface not wired in Services.yaml"
missing=1
fi
done <<<"$ifaces"
[ "$missing" -eq 1 ] && exit 1
exit 0
severity: error
desc: "Adapter interfaces must be wired in Services.yaml (alias or factory) for dependency injection to resolve them"
tags: [architecture, di, services-yaml, adapter]
- id: TC-173
name: concrete-adapter-typehint
type: command
command: |
# Find concrete adapter implementations (non-interface PHP files in Adapter/)
adapters=$(find Classes/Adapter -name '*.php' ! -name '*Interface.php' ! -name '*Factory.php' 2>/dev/null | sed 's|Classes/||;s|\.php||;s|/|\\\\|g')
[ -z "$adapters" ] && exit 0
violations=0
for adapter in $adapters; do
shortname=$(echo "$adapter" | grep -oP '[^\\]+$')
# Check if concrete adapter class is type-hinted in constructors outside Adapter/
if grep -rnP "(private|protected|public)\s+(readonly\s+)?${shortname}\s+" Classes/ 2>/dev/null | grep -vP 'Adapter/|Factory/' | head -3; then
echo "WARN: Concrete adapter class $shortname type-hinted in constructor — use interface instead"
violations=1
fi
done
[ "$violations" -gt 0 ] && exit 1
exit 0
severity: warning
desc: "Constructors should type-hint adapter interfaces, not concrete adapter implementations"
tags: [architecture, di, adapter, type-hint]
# === PHPSTAN MULTI-VERSION COMPATIBILITY CHECKS ===
- id: TC-174
name: phpstan-version-specific-ignore
type: command
command: |
# Check for @phpstan-ignore tags in PHP files that reference version-specific issues
found=$(grep -rnP '@phpstan-ignore\s+(method\.notFound|call\.notFound|argument\.type)' Classes/ 2>/dev/null | head -5)
if [ -n "$found" ]; then
# Only flag if multi-version deps exist
multi_deps=$(grep -oP '"[^"]+"\s*:\s*"[^"]*\|\|[^"]*"' composer.json 2>/dev/null | grep -vP '^"php"\s*:|^"typo3/cms-[^"]*"\s*:')
if [ -n "$multi_deps" ]; then
echo "$found"
echo "WARN: @phpstan-ignore tags for method/call/argument issues may be version-specific — use adapter pattern instead"
exit 1
fi
fi
exit 0
severity: warning
desc: "PHPStan ignore tags for method.notFound/call.notFound/argument.type suggest version-specific code — use adapter pattern instead"
tags: [phpstan, multi-version, ignore-tags]
- id: TC-175
name: method-exists-typed-parameter
type: command
command: |
# Check for method_exists() calls with typed (non-object) first argument
# This causes PHPStan type narrowing issues across versions
found=$(grep -rnP 'method_exists\s*\(\s*\$' Classes/ 2>/dev/null | head -10)
[ -z "$found" ] && exit 0
# Check if any of these are inside methods with typed parameters (not object type)
violations=0
while IFS= read -r line; do
file=$(echo "$line" | cut -d: -f1)
varname=$(echo "$line" | grep -oP 'method_exists\s*\(\s*\$\K[a-zA-Z_]+')
# Check if the variable has a concrete type hint (not object) in the method signature
if grep -P "(${varname})\s*[,)]" "$file" 2>/dev/null | grep -vP 'object\s+\$' | grep -P '[A-Z][a-zA-Z]+\s+\$'"${varname}" | head -1; then
echo "WARN: method_exists() used with typed parameter \$$varname — use 'object' type to avoid PHPStan narrowing issues"
violations=1
fi
done <<<"$found"
[ "$violations" -gt 0 ] && exit 1
exit 0
severity: warning
desc: "method_exists() for version detection should use 'object' type parameter to avoid PHPStan type narrowing across versions"
tags: [phpstan, multi-version, method-exists, type-narrowing]
# === MULTI-VERSION DEPENDENCY LLM REVIEWS ===
- id: TC-100f
name: multi-version-api-divergence
domain: architecture
prompt: |
Check for multi-version dependency compatibility:
1. Parse composer.json for dependencies with multi-major-version constraints
(e.g., "^3 || ^4", "^1.0 || ^2.0") — exclude php and typo3/cms-* packages
2. For each such dependency, identify API differences between the major versions
that could cause runtime errors (changed method signatures, removed classes,
renamed methods, changed constructor arguments)
3. Check if the extension uses an adapter/interface pattern to abstract these
differences, or if version-specific APIs are called directly in business logic
4. Verify that adapter implementations exist for EACH supported major version
5. Flag any direct usage of version-specific APIs outside of adapter classes
severity: error
desc: "Multi-version dependencies must use adapter pattern when APIs differ between major versions"
tags: [architecture, multi-version, adapter, dependency, assessment]
- id: TC-100g
name: phpstan-multi-version-analysis
domain: code-quality
prompt: |
Check PHPStan configuration and ignore tags for multi-version compatibility:
1. Check if PHPStan config includes analysis for all supported dependency versions
2. Find all @phpstan-ignore and @phpstan-ignore-next-line tags in Classes/
3. Determine if any ignore tags exist BECAUSE of version-specific API differences
(e.g., ignoring method.notFound for a method that only exists in one version)
4. These version-specific ignores will cause PHPStan errors when analyzed against
the other version — they must be replaced with adapter pattern
5. Check that PHPStan baseline does not contain version-specific suppressions
severity: warning
desc: "PHPStan ignore tags must not be version-specific — replace with adapter pattern for multi-version deps"
tags: [phpstan, multi-version, static-analysis, assessment]
- id: TC-100h
name: services-yaml-adapter-wiring
domain: architecture
prompt: |
Verify Services.yaml properly wires adapter interfaces for DI:
1. Find all interface files in Classes/Adapter/ or Classes/Service/ directories
2. Check Configuration/Services.yaml for explicit alias or factory wiring for
each adapter interface
3. Verify that constructor parameters throughout the codebase type-hint the
interface, NOT the concrete adapter implementation
4. If a factory pattern is used, verify the factory class exists and has a
create() method that performs version detection
5. Flag: interfaces without Services.yaml wiring, concrete type-hints where
interface exists, missing factory implementations
severity: error
desc: "Adapter interfaces must be wired in Services.yaml and type-hinted in constructors (not concrete classes)"
tags: [architecture, di, services-yaml, adapter, assessment]
- id: TC-100e
name: domain-model-validation
domain: architecture
prompt: |
Check domain models with cryptographic or hash fields for proper validation:
1. Find domain model classes in Classes/Domain/Model/ that contain properties
for hashes, checksums, signatures, tokens, or cryptographic values
2. Check if these models have a named constructor (static factory method)
that validates field consistency (e.g. verifying a checksum matches its
source data, or a signature is valid for its payload)
3. If models only have a default constructor with setters, there is no
guarantee of field consistency at creation time
4. Flag models with crypto fields that lack validation in their construction
5. Recommend implementing a named constructor pattern like
::createVerified() or ::fromPayload() that enforces invariants
severity: warning
desc: "Domain models with cryptographic fields should have a named constructor that validates field consistency"
tags: [architecture, domain-model, validation, security, assessment]
# === PSR-3 LOGGING COMPLIANCE (relocated from php-modernization PM-26) ===
- id: TC-180
name: psr3-logger-aware-nulllogger-default
domain: code-quality
prompt: |
Review the TYPO3 extension codebase for PSR-3 logging compliance.
Check for:
1. Classes implementing LoggerAwareInterface should use LoggerAwareTrait
2. Constructor should set $this->logger = new NullLogger() as default
3. A private getLogger(): LoggerInterface helper should exist to narrow
the nullable trait property for PHPStan
4. No nullable $logger properties with null checks scattered in methods
5. Logger should NOT be a required constructor parameter
Examine files in Classes/ directory. Report specific violations.
severity: warning
desc: "TYPO3 services must use LoggerAwareInterface with NullLogger default (not nullable)"
tags: [psr-3, logging, logger-aware, null-logger, typo3]
# === PSR-14 EVENT DISPATCH SAFETY (relocated from php-modernization PM-27) ===
- id: TC-181
name: psr14-event-dispatch-try-catch
domain: code-quality
prompt: |
Review the TYPO3 extension codebase for PSR-14 event dispatch safety.
Check for:
1. Event dispatch calls that are NOT wrapped in try/catch for post-processing
or notification events (listener failures must not break main flow)
2. Validation events that correctly do NOT catch exceptions (listeners may
legitimately stop flow)
3. Caught exceptions should be logged with context (event class, exception)
4. Event classes should be final with readonly constructor properties
Examine files in Classes/ directory. Report unguarded dispatches that should
be guarded, and guarded dispatches that should not be.
severity: warning
desc: "PSR-14 event dispatch must be try/catch guarded for non-validation events"
tags: [psr-14, events, event-dispatcher, typo3]
# === FACTORY PATTERN + PHPSTAN BASELINE PRACTICES (relocated from php-modernization PM-28) ===
- id: TC-182
name: factory-capability-fallback-baseline
domain: code-quality
prompt: |
Review the TYPO3 extension for factory pattern and PHPStan baseline practices.
Check for:
1. Factory classes that select implementations at runtime should have explicit
fallback chains (e.g., Imagick -> GD -> RuntimeException)
2. Services.yaml should wire factories correctly using the factory key
3. PHPStan baseline (phpstan-baseline.neon) should be shrinking, not growing
4. No new baseline entries should be added — issues should be fixed immediately
5. ProcessorInterface pattern: services with process() + canProcess() for
middleware-style decoupling
Report specific issues with factory wiring, baseline growth, or missing
processor interfaces.
severity: info
desc: "Factory patterns must have capability fallback; PHPStan baseline must shrink"
tags: [factory, services-yaml, phpstan, baseline, processor-interface, typo3]
# === MAKEINSTANCE TO DEPENDENCY INJECTION (relocated from php-modernization PM-33) ===
- id: TC-183
name: makeinstance-in-di-managed-classes
domain: code-quality
prompt: |
Review the codebase for GeneralUtility::makeInstance() calls in classes
that are registered in Configuration/Services.yaml (or Services.php).
Classes managed by the DI container should use constructor injection instead
of GeneralUtility::makeInstance(). Check for:
1. Classes listed in Services.yaml that call GeneralUtility::makeInstance()
2. Suggest constructor injection for each dependency resolved via makeInstance()
3. EXCEPTION: Scheduler task classes extending AbstractTask — these are
unserialized by the TYPO3 scheduler, so constructor DI is incompatible.
Do NOT flag makeInstance() in AbstractTask subclasses.
4. Also acceptable: makeInstance() for value objects or non-service classes
that are not in the DI container (e.g., DataHandler, FlashMessage).
Report specific files and line numbers where makeInstance() should be
replaced with constructor injection.
severity: warning
desc: "GeneralUtility::makeInstance() in DI-managed classes should use constructor injection (except AbstractTask)"
tags: [di, dependency-injection, makeinstance, typo3, services]
- id: TC-184
name: exception-handler-public-di-service
domain: code-quality
prompt: |
Check Configuration/Services.yaml for any class that extends
TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler
or implements TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface.
These classes are resolved by GeneralUtility::makeInstance() inside
ContentObjectRenderer::createExceptionHandler() with no constructor arguments.
makeInstance() only routes through the DI container (and thus performs
constructor injection) when the service is declared public: true. Without it,
makeInstance() falls back to `new ClassName()` with zero arguments, which
throws ArgumentCountError at runtime when a content element exception occurs.
Verify that every such class is explicitly declared in Configuration/Services.yaml with:
public: true
shared: false # matches ProductionExceptionHandler's own declaration
Report any exception handler class that is missing this explicit declaration,
even if it appears to be covered by the namespace wildcard resource glob.
severity: error
desc: "TYPO3 content exception handler classes must be declared public: true and shared: false in Configuration/Services.yaml (required for GeneralUtility::makeInstance() DI resolution)"
tags: [di, dependency-injection, exception-handler, services, makeinstance, typo3]
TYPO3 v13 Backend Module Modernization
Purpose: Comprehensive guide for modernizing TYPO3 backend modules to v13 LTS standards Source: Real-world modernization of nr_temporal_cache backend module (45/100 → 95/100 compliance) Target: TYPO3 v13.4 LTS with PSR-12, modern JavaScript, and accessibility compliance
---
Critical Compliance Issues
1. Extension Key Consistency
Problem: Mixed extension keys throughout templates and JavaScript breaks translations and routing
Common Violations:
- Template translation keys using
EXT:wrong_name/instead ofEXT:correct_name/ - JavaScript alert messages with hardcoded wrong extension names
- Variable substitution using wrong extension prefix
Detection:
# Find all extension key references
grep -rn "EXT:temporal_cache/" Resources/Private/Templates/
grep -rn "EXT:temporal_cache/" Resources/Public/JavaScript/
# Verify correct key in ext_emconf.php
grep "\$EM_CONF\[" ext_emconf.phpExample Violations:
<!-- WRONG: Using temporal_cache instead of nr_temporal_cache -->
<f:translate key="LLL:EXT:temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" />
<!-- CORRECT: Using proper extension key -->
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" />// WRONG: Hardcoded wrong extension name in alert
alert('Error in Temporal Cache extension');
// CORRECT: Use TYPO3 Notification API with correct name
Notification.error('Error', 'Failed in nr_temporal_cache');Impact: Broken translations, 404 errors on static assets, module registration failures
Severity: 🔴 Critical - Breaks basic functionality
Fix Priority: Immediate - Fix before any other modernization work
---
2. CSP Compliance: No Inline Scripts or Styles
Problem: Inline <script> and <style> blocks violate Content Security Policy (CSP). TYPO3 v13+ backend enforces CSP by default, so inline code will be blocked silently.
TYPO3 v13 Standard: Move ALL inline JavaScript and CSS to external files. Load via f:be.pageRenderer attributes or PageRenderer PHP API.
Before (CSP VIOLATION):
<f:section name="Content">
<style>
.my-module .status { color: green; }
</style>
<!-- template content -->
</f:section>
<f:section name="FooterAssets">
<script>
document.addEventListener('DOMContentLoaded', function() { /* ... */ });
</script>
</f:section>After (CSP-COMPLIANT):
<!-- Resources/Private/Layouts/Module.html -->
<f:be.pageRenderer
includeCssFiles="{0: 'EXT:my_extension/Resources/Public/Css/BackendModule.css'}"
includeJsFiles="{0: 'EXT:my_extension/Resources/Public/JavaScript/BackendModule.js'}"
/>Or via PHP in the controller:
$moduleTemplate->getPageRenderer()->addCssFile(
'EXT:my_extension/Resources/Public/Css/BackendModule.css'
);
$moduleTemplate->getPageRenderer()->loadJavaScriptModule(
'@vendor/my-extension/backend-module.js'
);WARNING: `f:be.pageRenderer` Attribute Name Gotcha
>
The correct attribute for JavaScript files is `includeJsFiles`, NOT includeJavaScriptFiles.Fluid silently ignores unknown ViewHelper attributes, so using the wrong name produces
no error and no output — your JS simply will not load. This is a common and hard-to-debug mistake.
>
```html
<!-- WRONG: silently fails, no error, no JS loaded -->
<f:be.pageRenderer includeJavaScriptFiles="{0: 'EXT:my_ext/...'}" />
>
<!-- CORRECT: loads the JS file -->
<f:be.pageRenderer includeJsFiles="{0: 'EXT:my_ext/...'}" />
```
>
Similarly, useincludeCssFiles(notincludeCssStylesheetFilesor similar).
Detection:
# Check for inline scripts/styles in templates (CSP violations)
grep -rn "<script" Resources/Private/Templates/
grep -rn "<style" Resources/Private/Templates/
# Check for wrong f:be.pageRenderer attribute names (silent failures)
grep -rn "includeJavaScriptFiles" Resources/Private/Impact: CSP compliance, prevents silent JS loading failures, better caching, maintainability
Severity: 🔴 Critical - TYPO3 v13 backend CSP enforcement blocks inline code silently
---
3. JavaScript Modernization (ES6 Modules)
Problem: Inline JavaScript in templates is deprecated, not CSP-compliant, and hard to maintain
TYPO3 v13 Standard: All JavaScript must be ES6 modules loaded via PageRenderer
Before (DEPRECATED):
<!-- Resources/Private/Templates/Backend/TemporalCache/Content.html -->
<f:section name="Content">
<!-- Template content -->
</f:section>
<f:section name="FooterAssets">
<script type="text/javascript">
// 68 lines of inline JavaScript
document.addEventListener('DOMContentLoaded', function() {
const selectAll = document.getElementById('select-all');
const checkboxes = document.querySelectorAll('.content-checkbox');
const harmonizeBtn = document.getElementById('harmonize-btn');
selectAll.addEventListener('change', function(e) {
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
harmonizeBtn.addEventListener('click', function() {
if (confirm('Really harmonize?')) {
// AJAX call with alert() feedback
}
});
});
</script>
</f:section>After (MODERN v13):
Step 1: Create ES6 Module (Resources/Public/JavaScript/BackendModule.js)
/**
* Backend module JavaScript for nr_temporal_cache
* TYPO3 v13 ES6 module
*/
import Modal from '@typo3/backend/modal.js';
import Notification from '@typo3/backend/notification.js';
class TemporalCacheModule {
constructor() {
this.initializeEventListeners();
}
initializeEventListeners() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.init());
} else {
this.init();
}
}
init() {
this.initializeHarmonization();
this.initializeKeyboardNavigation();
}
initializeHarmonization() {
const selectAllCheckbox = document.getElementById('select-all');
const contentCheckboxes = document.querySelectorAll('.content-checkbox');
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
if (!harmonizeBtn) return;
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
contentCheckboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
this.updateHarmonizeButton();
});
}
contentCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => this.updateHarmonizeButton());
});
harmonizeBtn.addEventListener('click', () => this.performHarmonization());
}
async performHarmonization() {
const selectedUids = Array.from(document.querySelectorAll('.content-checkbox:checked'))
.map(cb => parseInt(cb.dataset.uid));
if (selectedUids.length === 0) return;
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
const harmonizeUri = harmonizeBtn.dataset.actionUri;
// Use TYPO3 Modal instead of confirm()
Modal.confirm(
'Confirm Harmonization',
`Harmonize ${selectedUids.length} content elements?`,
Modal.SeverityEnum.warning,
[
{
text: 'Cancel',
active: true,
btnClass: 'btn-default',
trigger: () => Modal.dismiss()
},
{
text: 'Harmonize',
btnClass: 'btn-warning',
trigger: () => {
Modal.dismiss();
this.executeHarmonization(harmonizeUri, selectedUids);
}
}
]
);
}
async executeHarmonization(uri, selectedUids) {
try {
const response = await fetch(uri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: selectedUids, dryRun: false })
});
const data = await response.json();
if (data.success) {
// Use TYPO3 Notification API instead of alert()
Notification.success('Harmonization Successful', data.message);
setTimeout(() => window.location.reload(), 1500);
} else {
Notification.error('Harmonization Failed', data.message);
}
} catch (error) {
Notification.error('Error', 'Failed to harmonize content: ' + error.message);
}
}
initializeKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + A: Select all
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
const selectAll = document.getElementById('select-all');
if (selectAll && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
selectAll.checked = true;
selectAll.dispatchEvent(new Event('change'));
}
}
});
}
}
// Initialize and export
export default new TemporalCacheModule();Step 2: Load Module in Controller (Classes/Controller/Backend/TemporalCacheController.php)
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle(
$this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab')
);
// Load JavaScript module
$moduleTemplate->getPageRenderer()->loadJavaScriptModule(
'@netresearch/nr-temporal-cache/backend-module.js'
);
// Add DocHeader buttons
$this->addDocHeaderButtons($moduleTemplate, $currentAction);
// ...
}Step 3: Remove Inline JavaScript from Templates
<!-- Resources/Private/Templates/Backend/TemporalCache/Content.html -->
<f:section name="Content">
<!-- Template content with data attributes for JavaScript -->
<button
type="button"
class="btn btn-success"
id="harmonize-selected-btn"
disabled
data-action="harmonize"
data-action-uri="{harmonizeActionUri}"
aria-label="{f:translate(key: '...:content.harmonize_selected')}">
<core:icon identifier="actions-synchronize" size="small" />
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.harmonize_selected" />
</button>
</f:section>
<!-- FooterAssets section removed completely -->Validation:
# Ensure NO inline JavaScript remains
grep -rn "FooterAssets" Resources/Private/Templates/
grep -rn "<script" Resources/Private/Templates/
# Verify ES6 module exists
ls -lh Resources/Public/JavaScript/BackendModule.js
# Check controller loads module
grep "loadJavaScriptModule" Classes/Controller/Backend/*.phpImpact: CSP compliance, better caching, maintainability, modern development patterns
Severity: 🟡 Important - Required for TYPO3 v13 compliance
---
4. Module Layout Pattern
Problem: Old Default.html layout is non-standard for TYPO3 v13
TYPO3 v13 Standard: Use dedicated Module.html layout for backend modules
Before (NON-STANDARD):
<!-- Resources/Private/Templates/Backend/TemporalCache/Dashboard.html -->
<f:layout name="Default" />
<f:section name="Content">
<h1>Dashboard</h1>
<!-- Content -->
</f:section>After (MODERN v13):
Step 1: Create Module Layout (Resources/Private/Layouts/Module.html)
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:be.pageRenderer />
<div class="module" data-module-name="temporal-cache">
<f:render section="Before" optional="true" />
<div class="module-body">
<f:flashMessages />
<f:render section="Content" />
</div>
<f:render section="After" optional="true" />
</div>
</html>Step 2: Update All Templates
<!-- Resources/Private/Templates/Backend/TemporalCache/Dashboard.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Module" />
<f:section name="Content">
<h1><f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:dashboard.title" /></h1>
<!-- Content -->
</f:section>
</html>Validation:
# Check all templates use Module layout
grep -n "f:layout name=" Resources/Private/Templates/Backend/**/*.html
# Verify Module.html exists
ls -l Resources/Private/Layouts/Module.html
# Ensure no Default.html dependencies
! grep -r "Default.html" Resources/Private/Templates/Severity: 🟡 Important - Standard TYPO3 v13 pattern
---
5. DocHeader Component Integration
Problem: Backend modules should have standard DocHeader with refresh, shortcut, and action-specific buttons
TYPO3 v13 Standard: Use ButtonBar, IconFactory for DocHeader components
Before (MISSING):
// Classes/Controller/Backend/TemporalCacheController.php
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle('Temporal Cache');
// No DocHeader buttons
}After (MODERN v13):
Step 1: Add Required Imports
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;Step 2: Inject IconFactory
public function __construct(
private readonly ModuleTemplateFactory $moduleTemplateFactory,
private readonly ExtensionConfiguration $extensionConfiguration,
// ... other dependencies
private readonly IconFactory $iconFactory // ADD THIS
) {}Step 3: Add DocHeader Buttons Method
private function addDocHeaderButtons(ModuleTemplate $moduleTemplate, string $currentAction): void
{
if (!isset($this->uriBuilder)) {
return; // Skip in tests
}
$buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
// Refresh button (all actions)
$refreshButton = $buttonBar->makeLinkButton()
->setHref($this->uriBuilder->reset()->uriFor($currentAction))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL))
->setShowLabelText(false);
$buttonBar->addButton($refreshButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
// Shortcut/bookmark button (all actions)
$shortcutButton = $buttonBar->makeShortcutButton()
->setRouteIdentifier('tools_TemporalCache')
->setDisplayName($this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab'))
->setArguments(['action' => $currentAction]);
$buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
// Action-specific buttons
switch ($currentAction) {
case 'dashboard':
// Quick access to content list
$contentButton = $buttonBar->makeLinkButton()
->setHref($this->uriBuilder->reset()->uriFor('content'))
->setTitle($this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:button.view_content'))
->setIcon($this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL))
->setShowLabelText(true);
$buttonBar->addButton($contentButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
break;
case 'wizard':
// Help button for wizard
$helpButton = $buttonBar->makeHelpButton()
->setFieldName('temporal_cache_wizard')
->setModuleName('_MOD_tools_TemporalCache');
$buttonBar->addButton($helpButton, ButtonBar::BUTTON_POSITION_RIGHT, 3);
break;
}
}Step 4: Call from setupModuleTemplate
private function setupModuleTemplate(ModuleTemplate $moduleTemplate, string $currentAction): void
{
$moduleTemplate->setTitle(
$this->getLanguageService()->sL('LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab')
);
$moduleTemplate->getPageRenderer()->loadJavaScriptModule(
'@netresearch/nr-temporal-cache/backend-module.js'
);
// Add DocHeader buttons
$this->addDocHeaderButtons($moduleTemplate, $currentAction);
// ... menu creation
}Validation:
# Check IconFactory injection
grep "IconFactory" Classes/Controller/Backend/*.php
# Verify addDocHeaderButtons method exists
grep -A 5 "addDocHeaderButtons" Classes/Controller/Backend/*.php
# Check button types used
grep "makeLinkButton\|makeShortcutButton\|makeHelpButton" Classes/Controller/Backend/*.phpCommon Button Types:
makeLinkButton()- Navigate to URLmakeShortcutButton()- Bookmark module statemakeHelpButton()- Context-sensitive helpmakeInputButton()- Form submissionmakeFullyRenderedButton()- Custom HTML
Severity: 🟡 Important - Standard TYPO3 UX pattern
---
6. TYPO3 Modal and Notification APIs
Problem: Browser alert(), confirm(), prompt() are deprecated and not user-friendly
TYPO3 v13 Standard: Use @typo3/backend/modal.js and @typo3/backend/notification.js
Before (DEPRECATED):
// Inline JavaScript using browser APIs
if (confirm('Really delete this item?')) {
fetch('/delete', { method: 'POST' })
.then(() => alert('Deleted successfully'))
.catch(() => alert('Error occurred'));
}After (MODERN v13):
import Modal from '@typo3/backend/modal.js';
import Notification from '@typo3/backend/notification.js';
// Confirmation Modal
Modal.confirm(
'Delete Item',
'Really delete this item? This action cannot be undone.',
Modal.SeverityEnum.warning,
[
{
text: 'Cancel',
active: true,
btnClass: 'btn-default',
trigger: () => Modal.dismiss()
},
{
text: 'Delete',
btnClass: 'btn-danger',
trigger: () => {
Modal.dismiss();
performDelete();
}
}
]
);
async function performDelete() {
try {
const response = await fetch('/delete', { method: 'POST' });
const data = await response.json();
if (data.success) {
Notification.success('Success', 'Item deleted successfully');
} else {
Notification.error('Error', data.message);
}
} catch (error) {
Notification.error('Error', 'Failed to delete: ' + error.message);
}
}Modal Severity Levels:
Modal.SeverityEnum.notice- Info/notice (blue)Modal.SeverityEnum.info- Information (blue)Modal.SeverityEnum.ok- Success (green)Modal.SeverityEnum.warning- Warning (yellow)Modal.SeverityEnum.error- Error (red)
Notification Types:
Notification.success(title, message, duration)- Green success messageNotification.error(title, message, duration)- Red error messageNotification.warning(title, message, duration)- Yellow warningNotification.info(title, message, duration)- Blue informationNotification.notice(title, message, duration)- Gray notice
Validation:
# Check for browser APIs (violations)
grep -rn "alert(" Resources/Public/JavaScript/
grep -rn "confirm(" Resources/Public/JavaScript/
grep -rn "prompt(" Resources/Public/JavaScript/
# Verify TYPO3 APIs used
grep "import.*Modal" Resources/Public/JavaScript/*.js
grep "import.*Notification" Resources/Public/JavaScript/*.jsSeverity: 🟡 Important - Modern UX and consistency
---
7. Bootstrap 5 Migration Patterns
Problem: TYPO3 v13 backend uses Bootstrap 5. Extensions still using Bootstrap 4 data attributes and classes will have non-functional UI components (e.g., dismiss buttons, dropdowns, modals).
Bootstrap 4 → 5 Migration Map:
| Bootstrap 4 (WRONG) | Bootstrap 5 (CORRECT) | Impact |
|---|---|---|
data-dismiss="modal" | data-bs-dismiss="modal" | Dismiss buttons stop working |
data-toggle="dropdown" | data-bs-toggle="dropdown" | Dropdowns stop working |
data-toggle="collapse" | data-bs-toggle="collapse" | Collapsible panels stop working |
data-target="#id" | data-bs-target="#id" | Target references break |
btn-default | btn-secondary | Button renders unstyled |
bg-success + style="color: #fff" | text-bg-success | Use combined text-bg utility |
bg-warning text-dark | text-bg-warning | Use combined text-bg utility |
bg-danger + inline color | text-bg-danger | Use combined text-bg utility |
ml-* / mr-* | ms-* / me-* | Margin classes renamed (logical properties) |
pl-* / pr-* | ps-* / pe-* | Padding classes renamed (logical properties) |
float-left / float-right | float-start / float-end | Float utilities renamed |
Before (BOOTSTRAP 4 - BROKEN in TYPO3 v13):
<div class="alert alert-success bg-success" style="color: #fff;">
<button type="button" class="close" data-dismiss="alert">×</button>
Operation completed
</div>
<button class="btn btn-default ml-2">Cancel</button>After (BOOTSTRAP 5 - CORRECT):
<div class="alert alert-success text-bg-success">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
Operation completed
</div>
<button class="btn btn-secondary ms-2">Cancel</button>Detection:
# Find Bootstrap 4 data attributes (must be migrated)
grep -rn 'data-dismiss\|data-toggle\|data-target\|data-ride' Resources/Private/
# Find deprecated Bootstrap 4 classes
grep -rn 'btn-default\|ml-[0-9]\|mr-[0-9]\|pl-[0-9]\|pr-[0-9]\|float-left\|float-right' Resources/Private/
# Find inline color styles that should use text-bg-* utilities
grep -rn 'bg-success.*color\|bg-warning.*text-dark\|bg-danger.*color' Resources/Private/Severity: 🟡 Important - Non-functional UI components in TYPO3 v13 backend
---
8. Accessibility (ARIA Labels and Roles)
Problem: Backend modules must be accessible for screen readers and keyboard navigation
WCAG 2.1 AA Requirements:
- Semantic HTML roles
- ARIA labels on interactive elements
- Keyboard navigation support
Before (MISSING ACCESSIBILITY):
<table class="table table-striped">
<thead>
<tr>
<th>
<input type="checkbox" id="select-all">
</th>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="content-checkbox"></td>
<td>Content item</td>
</tr>
</tbody>
</table>After (ACCESSIBLE v13):
<table class="table table-striped table-hover" role="grid" aria-label="Temporal Content List">
<thead>
<tr role="row">
<th style="width: 40px;" role="columnheader">
<input
type="checkbox"
id="select-all"
class="form-check-input"
aria-label="Select all content items">
</th>
<th role="columnheader">
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.table.title" />
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input
type="checkbox"
class="form-check-input content-checkbox"
data-uid="{item.content.uid}"
aria-label="Select content item: {item.content.title}">
</td>
<td>{item.content.title}</td>
</tr>
</tbody>
</table>Accessible Button Example:
<button
type="button"
class="btn btn-success"
id="harmonize-selected-btn"
disabled
data-action="harmonize"
aria-label="{f:translate(key: '...:content.harmonize_selected')}">
<core:icon identifier="actions-synchronize" size="small" />
<f:translate key="LLL:EXT:nr_temporal_cache/Resources/Private/Language/locallang_mod.xlf:content.harmonize_selected" />
</button>Required ARIA Attributes:
role="grid"- On data tablesrole="row"- On table rowsrole="columnheader"- On table headersaria-label="..."- On interactive elements without visible textaria-labelledby="..."- Reference to label elementaria-describedby="..."- Additional description
Keyboard Navigation:
// Support Ctrl+A for select all
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
const selectAll = document.getElementById('select-all');
if (selectAll && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
selectAll.checked = true;
selectAll.dispatchEvent(new Event('change'));
}
}
});Validation:
# Check for ARIA labels
grep -rn "aria-label" Resources/Private/Templates/
# Check for semantic roles
grep -rn 'role="grid\|row\|columnheader"' Resources/Private/Templates/
# Verify keyboard navigation support
grep -rn "keydown\|keyup\|keypress" Resources/Public/JavaScript/Severity: 🟢 Recommended - WCAG 2.1 AA compliance
---
9. Icon Registration (Configuration/Icons.php)
Problem: Icon registration in ext_localconf.php using IconRegistry is deprecated in TYPO3 v13
TYPO3 v13 Standard: Use Configuration/Icons.php return array
Before (DEPRECATED v13):
// ext_localconf.php - DEPRECATED
$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Core\Imaging\IconRegistry::class
);
$iconRegistry->registerIcon(
'temporal-cache-module',
\TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
['source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Extension.svg']
);After (MODERN v13):
<?php
// Configuration/Icons.php
declare(strict_types=1);
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
return [
'temporal-cache-module' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Extension.svg',
],
'temporal-cache-harmonize' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:nr_temporal_cache/Resources/Public/Icons/Harmonize.svg',
],
];Validation:
# Check for deprecated IconRegistry usage
grep -rn "IconRegistry" ext_localconf.php ext_tables.php
# Verify Configuration/Icons.php exists
ls -l Configuration/Icons.php
# Check icon registration format
grep -A 3 "return \[" Configuration/Icons.phpSeverity: 🟡 Important - Removes deprecation warnings
---
10. CSRF Protection (URI Generation)
Problem: Hardcoded action URLs bypass TYPO3 CSRF protection
TYPO3 v13 Standard: Use uriBuilder for all action URIs
Before (INSECURE):
<button
id="harmonize-btn"
data-action-uri="/typo3/module/tools/temporal-cache/harmonize">
Harmonize
</button>const uri = button.dataset.actionUri;
fetch(uri, { method: 'POST', body: JSON.stringify(data) });After (SECURE v13):
Controller:
public function contentAction(?ServerRequestInterface $request = null, ...): ResponseInterface
{
// ...
$moduleTemplate->assignMultiple([
'content' => $paginator->getPaginatedItems(),
'harmonizeActionUri' => isset($this->uriBuilder)
? $this->uriBuilder->reset()->uriFor('harmonize')
: '',
// ...
]);
return $moduleTemplate->renderResponse('Backend/TemporalCache/Content');
}Template:
<button
id="harmonize-selected-btn"
data-action-uri="{harmonizeActionUri}">
Harmonize
</button>JavaScript:
const harmonizeBtn = document.getElementById('harmonize-selected-btn');
const harmonizeUri = harmonizeBtn.dataset.actionUri;
// URI includes CSRF token automatically
const response = await fetch(harmonizeUri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: selectedUids })
});Validation:
# Check for hardcoded URLs (violations)
grep -rn '"/typo3/' Resources/Private/Templates/
grep -rn '"/typo3/' Resources/Public/JavaScript/
# Verify uriBuilder usage in controller
grep "uriFor(" Classes/Controller/Backend/*.php
# Check template receives URIs
grep "Uri}" Resources/Private/Templates/Backend/**/*.htmlSeverity: 🔴 Critical - Security vulnerability
---
Complete Modernization Checklist
Phase 1: Extension Key Consistency (Critical)
- [ ] Verify correct extension key in
ext_emconf.php - [ ] Search and replace all
EXT:wrong_key/→EXT:correct_key/in templates - [ ] Update JavaScript alert/console messages with correct extension name
- [ ] Verify translation keys work in backend module
- [ ] Check static asset paths (CSS, images, icons)
Validation:
grep -rn "EXT:temporal_cache/" Resources/ # Should find ZERO
grep -rn "EXT:nr_temporal_cache/" Resources/ | wc -l # Should find ALLPhase 2: CSP Compliance (Critical)
- [ ] Remove ALL inline
<script>blocks from templates - [ ] Remove ALL inline
<style>blocks from templates - [ ] Move CSS to
Resources/Public/Css/BackendModule.css - [ ] Load CSS via
f:be.pageRenderer includeCssFilesorPageRenderer->addCssFile() - [ ] Verify
f:be.pageRendererusesincludeJsFiles(NOTincludeJavaScriptFiles)
Validation:
grep -rn "<script" Resources/Private/Templates/ # Should find ZERO
grep -rn "<style" Resources/Private/Templates/ # Should find ZERO
grep -rn "includeJavaScriptFiles" Resources/Private/ # Should find ZERO (wrong attribute)Phase 3: JavaScript Modernization (Important)
- [ ] Create
Resources/Public/JavaScript/BackendModule.jsas ES6 module - [ ] Import
@typo3/backend/modal.jsand@typo3/backend/notification.js - [ ] Implement class-based structure with proper initialization
- [ ] Replace all
alert()withNotificationAPI - [ ] Replace all
confirm()withModal.confirm() - [ ] Add keyboard navigation support (Ctrl+A, etc.)
- [ ] Remove ALL
<f:section name="FooterAssets">from templates - [ ] Load module via
$moduleTemplate->getPageRenderer()->loadJavaScriptModule()
Validation:
grep -rn "FooterAssets" Resources/Private/Templates/ # Should find ZERO
grep -rn "<script" Resources/Private/Templates/ # Should find ZERO
ls -lh Resources/Public/JavaScript/BackendModule.js # Should exist
grep "loadJavaScriptModule" Classes/Controller/Backend/*.php # Should find usagePhase 4: Layout Pattern (Important)
- [ ] Create
Resources/Private/Layouts/Module.htmlwith TYPO3 v13 structure - [ ] Add
xmlns:corenamespace to Module.html - [ ] Include
<f:flashMessages />in Module.html - [ ] Update ALL templates to use
<f:layout name="Module" /> - [ ] Add
xmlns:corenamespace to all templates - [ ] Remove any
Default.htmllayout dependencies
Validation:
ls -l Resources/Private/Layouts/Module.html # Should exist
grep -n "f:layout name=" Resources/Private/Templates/Backend/**/*.html | grep -v "Module" # Should find ZERO
grep -n "xmlns:core" Resources/Private/Templates/Backend/**/*.html | wc -l # Should match template countPhase 5: DocHeader Integration (Important)
- [ ] Add
use TYPO3\CMS\Backend\Template\Components\ButtonBar;import - [ ] Add
use TYPO3\CMS\Core\Imaging\Icon;import - [ ] Add
use TYPO3\CMS\Core\Imaging\IconFactory;import - [ ] Inject
IconFactoryinto controller constructor - [ ] Create
addDocHeaderButtons()method - [ ] Add refresh button (all actions)
- [ ] Add shortcut/bookmark button (all actions)
- [ ] Add action-specific buttons (view content, help, etc.)
- [ ] Call
addDocHeaderButtons()fromsetupModuleTemplate()
Validation:
grep "IconFactory" Classes/Controller/Backend/*.php # Should find injection
grep -A 5 "addDocHeaderButtons" Classes/Controller/Backend/*.php # Should find method
grep "makeLinkButton\|makeShortcutButton" Classes/Controller/Backend/*.php # Should find usagePhase 6: TYPO3 APIs (Important)
- [ ] Import
ModalandNotificationin ES6 module - [ ] Replace
confirm()withModal.confirm()with severity levels - [ ] Replace
alert()success withNotification.success() - [ ] Replace
alert()errors withNotification.error() - [ ] Use proper Modal button configurations (text, btnClass, trigger)
- [ ] Set appropriate severity levels (notice, info, ok, warning, error)
Validation:
grep -rn "alert(" Resources/Public/JavaScript/ # Should find ZERO
grep -rn "confirm(" Resources/Public/JavaScript/ # Should find ZERO
grep "Modal.confirm" Resources/Public/JavaScript/*.js # Should find usage
grep "Notification\.(success\|error)" Resources/Public/JavaScript/*.js # Should find usagePhase 7: Bootstrap 5 Migration (Important)
- [ ] Replace
data-dismisswithdata-bs-dismissin all templates - [ ] Replace
data-togglewithdata-bs-togglein all templates - [ ] Replace
data-targetwithdata-bs-targetin all templates - [ ] Replace
btn-defaultwithbtn-secondary - [ ] Replace
bg-success+ inline color withtext-bg-success - [ ] Replace
bg-warning text-darkwithtext-bg-warning - [ ] Replace
ml-*/mr-*withms-*/me-* - [ ] Replace
pl-*/pr-*withps-*/pe-* - [ ] Replace
float-left/float-rightwithfloat-start/float-end
Validation:
grep -rn 'data-dismiss\|data-toggle\|data-target' Resources/Private/ # Should find ZERO
grep -rn 'btn-default' Resources/Private/ # Should find ZERO
grep -rn 'ml-[0-9]\|mr-[0-9]' Resources/Private/ # Should find ZEROPhase 8: Accessibility (Recommended)
- [ ] Add
role="grid"to data tables - [ ] Add
role="row"to table rows - [ ] Add
role="columnheader"to table headers - [ ] Add
aria-labelto checkboxes without visible labels - [ ] Add
aria-labelto buttons with icon-only content - [ ] Implement keyboard navigation (Ctrl+A for select all)
- [ ] Test with screen reader
- [ ] Verify all interactive elements are keyboard accessible
Validation:
grep -rn "aria-label" Resources/Private/Templates/ # Should find accessibility labels
grep -rn 'role="grid\|row\|columnheader"' Resources/Private/Templates/ # Should find semantic rolesPhase 9: Icon Registration (Important)
- [ ] Create
Configuration/Icons.phpif missing - [ ] Migrate icon registration from
ext_localconf.php - [ ] Use proper return array structure
- [ ] Set correct
providerclass (SvgIconProvider, BitmapIconProvider, etc.) - [ ] Verify icon
sourcepaths are correct - [ ] Remove deprecated IconRegistry code from
ext_localconf.php
Validation:
ls -l Configuration/Icons.php # Should exist
grep -rn "IconRegistry" ext_localconf.php # Should find ZEROPhase 10: CSRF Protection (Critical)
- [ ] Use
uriBuilder->uriFor()for all action URIs - [ ] Pass URIs to templates via
assignMultiple() - [ ] Use data attributes in templates:
data-action-uri="{harmonizeActionUri}" - [ ] Read URIs from data attributes in JavaScript
- [ ] Remove all hardcoded
/typo3/...URLs
Validation:
grep -rn '"/typo3/' Resources/ # Should find ZERO (except maybe comments)
grep "uriFor(" Classes/Controller/Backend/*.php # Should find URI generationPhase 11: Testing and Validation (Critical)
- [ ] Run unit tests:
composer test:unit - [ ] Run functional tests:
composer test:functional - [ ] Check for PHP deprecation warnings
- [ ] Test module in TYPO3 backend manually
- [ ] Verify all buttons work (refresh, shortcut, action buttons)
- [ ] Test harmonization/actions with Modal confirmations
- [ ] Verify Notification API messages display correctly
- [ ] Test keyboard navigation (Ctrl+A, Tab order)
- [ ] Check browser console for JavaScript errors
- [ ] Validate translation keys work
Validation:
composer test # All tests should pass
vendor/bin/typo3 cache:flush # Clear caches
# Manual testing in backendPhase 12: Documentation (Important)
- [ ] Document backend module usage in
Documentation/ - [ ] Add screenshots of module UI
- [ ] Document keyboard shortcuts
- [ ] Update README.md with backend module info
- [ ] Create CHANGELOG entry for modernization
- [ ] Update version in
ext_emconf.php
---
Conformance Scoring Impact
| Category | Before | After | Improvement |
|---|---|---|---|
| Extension Architecture | 15/20 | 18/20 | +3 (Fixed extension keys, layout pattern) |
| Coding Guidelines | 18/20 | 20/20 | +2 (ES6 modules, icon registration) |
| PHP Architecture | 16/20 | 18/20 | +2 (IconFactory DI, proper URI generation) |
| Testing Standards | 18/20 | 18/20 | 0 (Already passing) |
| Best Practices | 15/20 | 20/20 | +5 (Modern JS, accessibility, CSRF) |
| Total Base Score | 82/100 | 94/100 | +12 points |
| Excellence: Documentation | 0/4 | 1/4 | +1 (Added module docs) |
| Total Score | 82/120 | 95/120 | +13 points |
Estimated Time Investment:
- Analysis: 1-2 hours
- Phase 1-2 (Critical fixes): 2-3 hours
- Phase 3-5 (JavaScript + Layout): 3-4 hours
- Phase 6-8 (Accessibility + Icons + CSRF): 2-3 hours
- Phase 9-10 (Testing + Docs): 2-3 hours
- Total: 10-15 hours for complete modernization
---
Common Pitfalls and Solutions
Pitfall 1: Extension Key Case Sensitivity
Problem: nr_temporal_cache vs nr-temporal-cache vs nrTemporalCache Solution: Use snake_case (nr_temporal_cache) consistently everywhere. TYPO3 extension keys are always snake_case.
Pitfall 2: JavaScript Module Path
Problem: @vendor/extension-name/ vs @vendor/extension_name/ Solution: Use hyphen-case in module paths: @netresearch/nr-temporal-cache/backend-module.js
Pitfall 3: Modal Not Dismissing
Problem: Modal stays open after button click Solution: Always call Modal.dismiss() in trigger callbacks before performing action
Pitfall 4: Notification Duration
Problem: Success notifications disappear too quickly Solution: Add duration parameter: Notification.success(title, message, 3) (3 seconds)
Pitfall 5: IconFactory Not Available in Tests
Problem: Tests fail with "Call to a member function getIcon() on null" Solution: Check isset($this->uriBuilder) before calling button-related methods
Pitfall 6: ARIA Labels Not Translatable
Problem: Hardcoded English text in aria-label Solution: Use Fluid translate ViewHelper: aria-label="{f:translate(key: '...')}"
---
Real-World Example: Complete Before/After
Extension: nr_temporal_cache - Backend module for temporal content management Modernization: Complete v13 compliance (45/100 → 95/100)
Files Changed
1. Classes/Controller/Backend/TemporalCacheController.php - Added IconFactory, DocHeader, JS module loading 2. Resources/Private/Layouts/Module.html - Created new layout 3. Resources/Private/Templates/Backend/TemporalCache/*.html - Fixed keys, removed inline JS, added ARIA 4. Resources/Public/JavaScript/BackendModule.js - Created 246-line ES6 module 5. Configuration/Icons.php - Created icon registration 6. ext_localconf.php - Removed deprecated IconRegistry code
Results
- ✅ 57 extension key references fixed
- ✅ 95 lines of inline JavaScript removed
- ✅ 246 lines of ES6 module created
- ✅ DocHeader with 3 button types added
- ✅ Modal and Notification APIs integrated
- ✅ ARIA labels on 12+ interactive elements
- ✅ Keyboard navigation (Ctrl+A) implemented
- ✅ CSRF protection via uriBuilder
- ✅ Zero deprecation warnings
- ✅ 316 unit tests passing
- ✅ 95/100 conformance score
Commit: 79db9cf - 6 files changed, +459/-204 lines, 8.1KB ES6 module created
---
References
- TYPO3 Core API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/
- Backend Module API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Backend/BackendModules.html
- JavaScript API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/JavaScript/
- Icon API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Icon/Index.html
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/
Created: 2025-11-21 based on real-world nr_temporal_cache modernization
Dual Version (v12 + v13) Compatibility Patterns
Source: netresearch/contexts extension conformance for TYPO3 12.4 + 13.4 LTS (2024-12)
Version Constraint Strategy
composer.json
{
"require": {
"php": "^8.2",
"typo3/cms-core": "^12.4 || ^13.4"
}
}ext_emconf.php
'constraints' => [
'depends' => [
'typo3' => '12.4.0-13.4.99',
'php' => '8.2.0-8.4.99',
],
],Critical Rector Configuration
DO NOT use UP_TO_TYPO3_13 when supporting both versions:
// rector.php - CORRECT for dual v12+v13
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_82,
Typo3LevelSetList::UP_TO_TYPO3_12, // NOT UP_TO_TYPO3_13!
Typo3SetList::CODE_QUALITY,
Typo3SetList::GENERAL,
]);Reason: UP_TO_TYPO3_13 introduces v13-only APIs that break v12 compatibility.
API Compatibility Decision Matrix
| Purpose | Use This (v12 Compatible) | Avoid (v13 Only) |
|---|---|---|
| Frontend user session | $TSFE->fe_user->getKey() | $request->getAttribute('frontend.user') |
| Page information | $data['pObj']->rootLine | $request->getAttribute('frontend.page.information') |
| Language | $TSFE->sys_language_uid | $request->getAttribute('language') |
| Site | $TSFE->getSite() | Works in both |
Compatibility Layer Pattern
When you need different behavior for v12 vs v13:
use TYPO3\CMS\Core\Information\Typo3Version;
class CompatibilityHelper
{
public static function isTypo3v13OrHigher(): bool
{
return (new Typo3Version())->getMajorVersion() >= 13;
}
public static function getPageId(ServerRequestInterface $request): int
{
if (self::isTypo3v13OrHigher()) {
$pageInfo = $request->getAttribute('frontend.page.information');
return $pageInfo?->getId() ?? 0;
}
// v12 fallback
$tsfe = $GLOBALS['TSFE'] ?? null;
return $tsfe?->id ?? 0;
}
}Testing Matrix Requirements
For enterprise-grade dual-version support:
| Test Type | Coverage Target |
|---|---|
| Unit Tests | 70%+ |
| Functional Tests | Key integrations |
| E2E Tests | Critical user journeys |
CI Matrix
matrix:
include:
- php: '8.2'
typo3: '^12.4'
- php: '8.3'
typo3: '^12.4'
- php: '8.2'
typo3: '^13.4'
- php: '8.3'
typo3: '^13.4'
- php: '8.4'
typo3: '^13.4'Conformance Scoring Adjustments
Base Score Modifications
When evaluating dual-version extensions:
| Criterion | Single Version | Dual Version |
|---|---|---|
| Uses v12 APIs only | Full points | Full points |
| Uses v13-only APIs | Full points | -10 points |
| Has version detection | +0 | +5 bonus |
| CI tests both versions | N/A | Required |
Excellence Indicators
Additional excellence points for dual-version:
| Indicator | Points |
|---|---|
| Matrix CI (both versions) | +3 |
| Compatibility layer documented | +2 |
| Version-specific documentation | +2 |
Documentation Requirements
Dual-version extensions must document:
1. Supported versions prominently in README 2. Installation differences (if any) 3. Feature parity (any v13-only features) 4. Migration path from single to dual version
Example README Section
## Compatibility
| TYPO3 | PHP | Status |
|-------|-----|--------|
| 13.4 LTS | 8.2 - 8.4 | Supported |
| 12.4 LTS | 8.2 - 8.3 | Supported |
| 11.5 LTS | 7.4 - 8.1 | Use v3.x |Checklist for Dual-Version Extensions
- [ ]
composer.jsonhas^12.4 || ^13.4constraint - [ ]
ext_emconf.phphas12.4.0-13.4.99constraint - [ ] Rector uses
UP_TO_TYPO3_12only - [ ] No v13-only request attributes used directly
- [ ] CI matrix tests both versions
- [ ] All tests pass on both versions
- [ ] Documentation states supported versions
- [ ] PHP minimum is 8.2 (required for v13)