TYPO3 Dual-Version Development: v13 & v14
Strategy: Write code that works on both TYPO3 v13 and v14, with v14 as the preferred target. All patterns in this skill are designed for dual-version compatibility.
- Version Strategy
Target: TYPO3 v13 and v14
Version Status PHP Support Until
v12.4 LTS Maintenance 8.1-8.3 April 2026
v13.4 LTS Active LTS 8.2-8.4 ~2028
v14.x Latest 8.2-8.4 ~2029
Best Practice: Target both v13 and v14 for maximum compatibility. New projects should prefer v14.
Version Constraints
'My Extension',
'version' => '2.0.0',
'state' => 'stable',
'constraints' => [
'depends' => [
'typo3' => '13.0.0-14.99.99',
'php' => '8.2.0-8.4.99',
],
'conflicts' => [],
'suggests' => [],
],
];
// composer.json - Dual version support
{
"name": "vendor/my-extension",
"type": "typo3-cms-extension",
"require": {
"php": "^8.2",
"typo3/cms-core": "^13.0 || ^14.0"
},
"extra": {
"typo3/cms": {
"extension-key": "my_extension"
}
}
}
2. PHP Requirements
Minimum PHP Version: 8.2
Both v13 and v14 require PHP 8.2+. Use modern PHP features:
doSomething(
name: 'value',
options: ['key' => 'value'],
);
// ✅ Match expressions (PHP 8.0+)
$type = match ($input) {
'a' => 'Type A',
'b' => 'Type B',
default => 'Unknown',
};
// ✅ Enums (PHP 8.1+)
enum Status: string
{
case Draft = 'draft';
case Published = 'published';
}
3. Controller Patterns (v13/v14 Compatible)
Extbase Action Controller
itemRepository->findAll();
$this->view->assign('items', $items);
return $this->htmlResponse();
}
public function showAction(int $item): ResponseInterface
{
$item = $this->itemRepository->findByUid($item);
$this->view->assign('item', $item);
return $this->htmlResponse();
}
// ✅ JSON response
public function apiAction(): ResponseInterface
{
$data = ['success' => true, 'items' => []];
return $this->jsonResponse(json_encode($data));
}
// ✅ Redirect
public function createAction(): ResponseInterface
{
// Process creation...
$this->addFlashMessage('Item created');
return $this->redirect('list');
}
}
Backend Module Controller
moduleTemplateFactory->create($request);
$moduleTemplate->assign('items', []);
return $moduleTemplate->renderResponse('Backend/Index');
}
}
4. View & Templating (v13/v14 Compatible)
ViewFactory (Preferred Pattern)
viewFactory->create($viewFactoryData);
$view->assignMultiple($data);
return $view->render('Notification');
}
}
Fluid Template Best Practices
{item.title}
{item.bodytext}
No items found.
View Details
5. Event System (v13/v14 Compatible)
PSR-14 Event Listeners
PSR-14 events are the standard in both v13 and v14. Always prefer events over hooks.
getPageId() === 123) {
$event->setCacheLifetime(300); // 5 minutes
}
}
}
Common Events (v13/v14)
Event Purpose
BeforeRecordOperationEvent Before DataHandler operations
AfterRecordOperationEvent After DataHandler operations
ModifyPageLinkConfigurationEvent Modify link building
ModifyCacheLifetimeForPageEvent Adjust page cache
BeforeStdWrapFunctionsInitializedEvent Modify stdWrap
Services.yaml Registration
# Configuration/Services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Vendor\Extension\:
resource: '../Classes/*'
exclude:
- '../Classes/Domain/Model/*'
# Event listener (alternative to #[AsEventListener] attribute)
Vendor\Extension\EventListener\MyListener:
tags:
- name: event.listener
identifier: 'vendor-extension/my-listener'
6. Backend Module Registration (v13/v14)
Configuration/Backend/Modules.php
[
'parent' => 'web',
'position' => ['after' => 'web_info'],
'access' => 'user,group',
'iconIdentifier' => 'myextension-module',
'path' => '/module/web/myextension',
'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf',
'extensionName' => 'MyExtension',
'controllerActions' => [
\Vendor\MyExtension\Controller\BackendController::class => [
'index',
'list',
'show',
],
],
],
];
Icon Registration
[
'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
'source' => 'EXT:my_extension/Resources/Public/Icons/module.svg',
],
];
7. TCA Configuration (v13/v14 Compatible)
Static TCA Only
In v14, runtime TCA modifications are forbidden. Always use static files:
[
'title' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_item',
'label' => 'title',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'delete' => 'deleted',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'searchFields' => 'title,description',
'iconIdentifier' => 'myextension-item',
],
'types' => [
'1' => [
'showitem' => '
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
title, description,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
hidden, starttime, endtime,
',
],
],
'columns' => [
'title' => [
'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_item.title',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
'required' => true,
],
],
'description' => [
'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myext_domain_model_item.description',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 5,
'enableRichtext' => true,
],
],
],
];
TCA Overrides
'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:ctype.title',
'value' => 'myextension_element',
'icon' => 'content-text',
'group' => 'default',
]
);
$GLOBALS['TCA']['tt_content']['types']['myextension_element'] = [
'showitem' => '
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
--palette--;;general,
header,
bodytext,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
--palette--;;hidden,
',
];
8. Database Operations (v13/v14 Compatible)
QueryBuilder
connectionPool->getQueryBuilderForTable('tx_myext_items');
return $queryBuilder
->select('*')
->from('tx_myext_items')
->where(
$queryBuilder->expr()->eq(
'status',
$queryBuilder->createNamedParameter($status)
)
)
->orderBy('title', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
public function countByPid(int $pid): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_myext_items');
return (int)$queryBuilder
->count('uid')
->from('tx_myext_items')
->where(
$queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
}
}
Extbase Repository
QueryInterface::ORDER_ASCENDING,
];
public function findPublished(): array
{
$query = $this->createQuery();
$query->matching(
$query->logicalAnd(
$query->equals('hidden', false),
$query->lessThanOrEqual('starttime', time()),
$query->logicalOr(
$query->equals('endtime', 0),
$query->greaterThan('endtime', time())
)
)
);
return $query->execute()->toArray();
}
}
9. CLI Commands (v13/v14 Compatible)
addArgument('type', InputArgument::REQUIRED, 'The type to process')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force processing');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
// Initialize backend for DataHandler operations
Bootstrap::initializeBackendAuthentication();
$type = $input->getArgument('type');
$force = $input->getOption('force');
$io->title('Processing: ' . $type);
// Your logic here...
$io->success('Processing completed successfully');
return Command::SUCCESS;
}
}
Command Registration
# Configuration/Services.yaml
services:
Vendor\Extension\Command\ProcessCommand:
tags:
- name: console.command
10. Testing for Dual-Version Compatibility
PHPUnit Setup
Tests/Unit
Tests/Functional
Test Both Versions in CI
.github/workflows/ci.yaml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
typo3: ['^13.0', '^14.0']
php: ['8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: intl, pdo_mysql
- name: Install dependencies
run: |
composer require typo3/cms-core:${{ matrix.typo3 }} --no-update
composer install --prefer-dist --no-progress
- name: Run tests
run: vendor/bin/phpunit
- Upgrade Process
From v12 to v13/v14
1. Create backup
ddev snapshot --name=before-upgrade
2. Update composer constraints
ddev composer require "typo3/cms-core:^13.0 || ^14.0" --no-update
ddev composer update "typo3/*" --with-all-dependencies
3. Run upgrade wizards
ddev typo3 upgrade:list
ddev typo3 upgrade:run
4. Clear caches
ddev typo3 cache:flush
5. Update database schema
ddev typo3 database:updateschema
6. Test thoroughly
- Resources
v13 Documentation: https://docs.typo3.org/m/typo3/reference-coreapi/13.4/en-us/
v14 Documentation: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/
v13 Changelog: https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog-13/Index.html
v14 Changelog: https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog-14/Index.html
TYPO3 Rector: https://github.com/sabbelasichon/typo3-rector
Credits & Attribution
This skill is based on the excellent TYPO3 best practices and methodology developed by Netresearch DTT GmbH. We are deeply grateful for their outstanding contributions to the TYPO3 community and their commitment to sharing knowledge.
Netresearch has been a leading force in TYPO3 development, and their expertise has been invaluable in shaping these guidelines. Thank you, Netresearch, for your exceptional work!
Copyright (c) Netresearch DTT GmbH - Methodology and best practices
Adapted by webconsulting.at for this skill collection