TYPO3 Core Changelog 

Extension key

core

Package name

typo3/cms-core

Version

main

Language

en

Author

TYPO3 contributors

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Wed, 10 Dec 2025 17:14:54 +0000


This extension provides the TYPO3 API, i.e. the core functionalities, which can be used and extended in any other TYPO3 extension.

Note: Every change to the TYPO3 Core which might affect your site is documented here.


Table of Contents:

ChangeLog v14 

Every change to the TYPO3 Core which might affect your site is documented here.

Also available 

14.1 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Feature: #107756 - Add QR Code module 

See forge#107756

Description 

A new Link Management > QR Codes backend module has been introduced, grouped alongside the existing Link Management > Redirects module.

The QR Codes module provides editors with an efficient way to generate reusable QR codes for various purposes, such as printing them on promotional materials, booth displays, or marketing collateral.

Each generated QR code contains a permanent, unique URL that never changes, ensuring printed materials remain valid indefinitely. While the QR code URL itself stays constant, the destination it redirects to can be updated at any time, providing flexibility to adapt campaigns or redirect visitors to current content without requiring reprints.

The module includes a convenient button to generate QR codes on demand, offering multiple download options including different formats (PNG, SVG) and customizable sizes to suit various use cases and printing requirements.

Impact 

The new QR Code module enables users to create scannable QR codes that redirect to any specified URL. This feature is particularly valuable for marketing campaigns, events, and printed materials where maintaining flexibility in the destination URL is essential while preserving the QR code itself.

Feature: #107837 - Route enhancers in site sets 

See forge#107837

Description 

Site sets can now define route enhancers in a dedicated route-enhancers.yaml file. This allows extensions to provide route enhancers as part of their site set configuration, which are automatically merged into the site configuration when the set is used as a dependency.

The route enhancers from site sets are applied as presets. This means that site-level route enhancer configuration takes precedence and can override set-defined enhancers.

Usage 

Create a route-enhancers.yaml file in your site set directory alongside the config.yaml:

EXT:my_extension/Configuration/Sets/MySet/
├── config.yaml
└── route-enhancers.yaml
Copied!

The file must contain a routeEnhancers key with the route enhancer definitions:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
routeEnhancers:
  MyEnhancer:
    type: Simple
    routePath: '/my-path/{param}'
    aspects:
      param:
        type: StaticValueMapper
        map:
          value1: '1'
          value2: '2'
Copied!

The route enhancers file supports YAML imports, allowing you to split configuration across multiple files:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
imports:
  - { resource: 'route-enhancers/*.yaml' }

routeEnhancers:
  # Additional enhancers can be defined here
Copied!

Merging behavior 

Route enhancers from site sets are merged in dependency order. When a site uses multiple sets, enhancers from earlier dependencies are loaded first, and later sets can override them.

Site-level route enhancer configuration always takes precedence over set-defined enhancers. This allows sites to customize or override preset configurations from sets.

Example 

Given a site set with:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
routeEnhancers:
  PageType:
    type: PageType
    default: '0'
    map:
      feed.xml: '100'
Copied!

And a site configuration with:

config/sites/my-site/config.yaml
dependencies:
  - my_extension/my-set

routeEnhancers:
  PageType:
    type: PageType
    default: '100'
    map:
      rss.xml: '200'
Copied!

The resulting configuration will be:

routeEnhancers:
  PageType:
    type: PageType
    default: '100'
    map:
      feed.xml: '100'
      rss.xml: '200'
Copied!

Scalar values from the site configuration override set-defined values, while new keys are appended.

Impact 

Extensions can now ship route enhancers as part of their site sets, providing a streamlined way to configure routing for extension functionality. This is particularly useful for extensions that require specific URL patterns, such as sitemap extensions or API endpoints.

Invalid route enhancer configurations are handled gracefully: sets with invalid route-enhancers.yaml files are skipped and logged, similar to other set validation errors.

Feature: #107837 - Sitemap route enhancers provided via site set 

See forge#107837

Description 

The SEO extension now ships its sitemap route enhancers as part of the typo3/seo-sitemap site set. When this set is used as a dependency, the route enhancers for XML sitemaps are automatically configured.

This enables clean URLs for sitemaps out of the box:

  • /sitemap.xml - Main sitemap index
  • /sitemap-type/pages - Pages sitemap

Previously, these route enhancers had to be manually configured in each site's config.yaml.

Impact 

Sites using the typo3/seo-sitemap set no longer need to manually configure sitemap route enhancers. The clean URLs are available automatically.

Sites can still override or extend the route enhancers in their site configuration if needed.

Feature: #107961 - Search translated pages in page tree 

See forge#107961

Description 

The page tree filter has been extended with the ability to search for pages through their translated content. This enhancement makes it significantly easier to find pages in multilingual TYPO3 installations, particularly when editors work primarily with translated content.

The page tree filter now supports two translation search methods:

  1. Search by translated page title: When a search phrase matches a translated page title or nav_title, the corresponding default language page will be found and displayed in the page tree.
  2. Search by translation UID: When searching for a numeric page UID that belongs to a translated page, the parent default language page will be found and displayed.

Both search methods work seamlessly alongside the existing search capabilities (searching by page title, nav_title, or default language UID).

Configuration 

Translation search is enabled by default and can be controlled in two ways:

User TSconfig 

Administrators can control the availability of translation search via User TSconfig:

# Disable searching in translated pages for specific users/groups
options.pageTree.searchInTranslatedPages = 0
Copied!

User Preference 

Individual backend users can toggle this setting using the page tree toolbar menu. The preference is stored in the backend user's configuration, allowing each user to customize their search behavior.

Visual Feedback 

When a page is found through a translation match, a colored label is automatically added to provide clear visual feedback:

Single translation match

Displays "Found in translation: [Language Name]"

Example: When searching for "Produkte", a page found via its German translation shows "Found in translation: German"

Multiple translation matches

Displays "Found in multiple translations"

Example: When searching for "Home", a page with matching French and German translations shows "Found in multiple translations"

Direct matches

Pages matching the search phrase directly (L=0) show "Search result"

Example: When searching for "Products", the English page titled "Products" shows "Search result"

Combined matches

When a page matches both directly and through a translation, both labels are displayed.

Example: Searching for "Home" finds a page titled "Home" with a German translation "Startseite Home" - the page shows both labels.

Flexibility for developers 

Developers can still use the PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent to add custom labels or modify the prepared tree items before they are rendered.

Impact 

Editors working in multilingual TYPO3 installations can now efficiently search for pages using translated titles or translation UIDs. The visual labels provide immediate feedback about how search results were matched, improving the user experience when navigating complex page trees.

The feature respects user permissions (language restrictions from user groups) and workspace context, ensuring that only accessible translations are searched.

Feature: #108344 - Allow number of decimals in stdWrap.bytes function 

See forge#108344

Description 

The TypoScript function stdWrap.bytes now accepts an additional configuration parameter decimals. It allows to explicitly define the number of decimals in the resulting number representation. By default, the number of decimals is derived from the formatted size.

In addition, the consumed PHP function TYPO3\CMS\Core\Utility\GeneralUtility::formatSize is extended as well. The additional parameter $decimals is added and defaults to null, which results in the same behavior as for the TypoScript function stdWrap.bytes.

Example 

lib.fileSize = TEXT
lib.fileSize {
    value = 123456
    bytes = 1
    bytes.decimals = 1
}
Copied!

Impact 

By allowing to configure the number of decimals in stdWrap.bytes, integrators can now better adapt the output of the formatted size returned by the TypoScript function. This was previously not possible by default and needed some workarounds in TypoScript.

Deprecation: #108086 - Raise deprecation error on using deprecated labels 

See forge#108086

Description 

Until now, localization labels marked as deprecated, for example by using the x-unused-since attribute in XLIFF 1.2 or the subState="deprecated" attribute in XLIFF 2.0, did not trigger a runtime warning. As a result, integrators and developers had no automatic way to detect deprecated labels that were still in use.

With this change, TYPO3 now triggers an E_USER_DEPRECATED error when a deprecated label is first written to the localization cache.

Impacted formats 

XLIFF 1.2 

<trans-unit id="deprecated_label" x-unused-since="4.5">
    <source>This label is deprecated</source>
</trans-unit>
Copied!

XLIFF 2.0 

<unit id="label5">
    <segment subState="deprecated">
        <source>This is label #5 (deprecated in English)</source>
    </segment>
</unit>
Copied!

Custom loaders 

When a label identifier ends with .x-unused, TYPO3 raises a deprecation warning if the label is referenced, regardless of whether the reference includes the .x-unused suffix.

Custom loaders can use this behaviour to also provide mechanisms to deprecate labels.

Fallback behaviour 

  • If a label is deprecated in the fallback language but is overridden in the current locale without a deprecation marker, no deprecation warning is raised.
  • If a label is deprecated only in the current locale, TYPO3 falls back to the default language and does not raise a deprecation warning.

A deprecation warning is triggered the first time a deprecated label is written to cache. Subsequent resolutions of the same label use the cached entry and do not trigger additional warnings until the cache is cleared.

The following usages emit a deprecation warning when a deprecated label is resolved.

LanguageService 

$this->languageService->sL('EXT:core/Resources/Private/Language/locallang.xlf:someDeprecation');
$this->languageService->sL('core.messages:someDeprecation');
Copied!

Fluid ViewHelper 

Usage of the f:translate ViewHelper in both Extbase and non-Extbase contexts:

<f:translate key="some_deprecation" domain="core.messages" />

<f:translate key="core.messages:some_deprecation" />

<f:translate key="EXT:core/Resources/Private/Language/locallang.xlf:some_deprecation" />
Copied!

The Extension Scanner does not detect the usage of deprecated localization labels. Developers must rely on runtime deprecation logs to identify these occurrences.

Impact 

Integrators and developers may encounter new deprecation warnings during runtime or in the deprecation log when deprecated localization labels are used. The warnings help identify and replace outdated labels before they are removed in a future TYPO3 version.

Affected installations 

All TYPO3 installations that use localization labels marked as deprecated are affected. This includes custom extensions, site packages, or integrations that still reference deprecated labels from system or extension language files.

When a custom extension or project defines a label whose identifier ends with .x-unused, that label is considered deprecated regardless of the loader used. Such usage is technically possible but generally unlikely.

Migration 

  1. Review the deprecation log for warnings related to localization labels. Note that deprecations are only written the first time a label is used after deleting the cache.
  2. Replace usages of deprecated labels with non-deprecated ones where possible.
  3. If required, override deprecated labels in a custom locale without a deprecation marker.
  4. Remove or update labels marked with x-unused-since in XLIFF 1.2 or with subState="deprecated" in XLIFF 2.0 when they are no longer needed.
  5. Avoid defining labels with identifiers ending in .x-unused.

Important: #107971 - XLF files now use 2-space indentation 

See forge#107971

Description 

TYPO3 Core XLF (XLIFF) translation files now consistently use 2-space indentation instead of tabs. This aligns with the formatting used by Crowdin and by PHP's \DOMDocument. The .editorconfig file has been updated accordingly.

In addition to indentation, all XLF files have been normalized using a unified XML formatter. This ensures consistent XML declaration, attribute ordering, and whitespace structure across all XLF files in the Core.

Checking and normalizing XLF formatting 

A new script has been introduced to check and normalize XLF formatting.

Checking formatting via runTests.sh 

To check whether XLF files have correct formatting (dry-run, no modifications):

./Build/Scripts/runTests.sh -s normalizeXliff -n
Copied!

This command scans all XLF files in typo3/sysext/ and reports files that would be changed.

Normalizing XLF files via runTests.sh 

To normalize all XLF files in-place:

./Build/Scripts/runTests.sh -s normalizeXliff
Copied!

This command applies consistent indentation and XML normalization to all XLF files in typo3/sysext/.

Using the standalone script 

The script can also be run directly. Requires PHP 8.2+ with DOM and intl extensions enabled.

# Show help
./Build/Scripts/xliffNormalizer.php --help

# Check files only (dry-run)
./Build/Scripts/xliffNormalizer.php --root typo3/sysext --dry-run

# Normalize files in place
./Build/Scripts/xliffNormalizer.php --root typo3/sysext

# Check a custom directory
./Build/Scripts/xliffNormalizer.php --root path/to/xlf/files --dry-run
Copied!

Impact 

Extension developers should update their XLF files to use 2-space indentation and expect normalized XML formatting. The provided script can also be used within extensions to keep XLF files consistent with TYPO3 Core standards.

14.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: #17406 - Field "url" in table "pages" has been removed 

See forge#17406

Description 

The former page type "External link" has been renamed to "Link" and now fully supports all typolink capabilities. It now uses the field link.

Since the field url was only used by this former page type, it has been removed.

Impact 

Custom code that expects the field url to exist in the table pages will fail as the field is not found anymore.

Affected installations 

  • TYPO3 projects that used the former page type External URL to link to resources other than true external URLs, for example sections such as #abc.
  • TYPO3 projects with custom code that expects the field url to exist in the table pages.

Migration 

The upgrade wizard "Migrate links of pages of type link" automatically migrates pages of the former type External URL to the new page type Link.

It migrates all links that resolve to external URLs. If your project used the External URL field for other purposes — for example, to link sections such as #abc — you must migrate those links manually.

If your project contains custom code that expects the field url to exist in the table pages, you can reintroduce this field via a TCA override, for example:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
<?php

defined('TYPO3') || die('Access restricted.');

$GLOBALS['TCA']['pages']['columns']['url'] = [
    'label' => 'External URL',
    'config' => [
        'type' => 'input',
        'size' => 50,
        'max' => 255,
        'required' => true,
        'eval' => 'trim',
        'softref' => 'url',
        'behaviour' => [
            'allowLanguageSynchronization' => true,
        ],
    ],
];

// Adding column to a existing or new palette and configure showitem,
// for a specific doktype the field is still required, for example:
$GLOBALS['TCA']['pages']['palettes]['custom_url'] = [
    'showitem' => 'url',
];
$GLOBALS['TCA']['pages']['types'][$customDokTypeValue] = [
    'showitem' => [
        --div--;core.form.tabs:general,
            doktype,
            --palette--;;title,
            --palette--;;custom_url,
    ],
];
Copied!

Adding it back at least ensures that the database field is not renamed and dropped by the Database Analyzer. In case TCA is not required while keeping the database field it could be added to a extension ext_tables.sql file.

Breaking: #33747 - Remove non-implemented sortable Collection logic 

See forge#33747

Description 

The \TYPO3\CMS\Core\Collection\SortableCollectionInterface has been removed from the TYPO3 Core.

This interface was never properly implemented and served no purpose in the codebase. It defined methods for sorting collections via callback functions and moving items within collections, but no concrete implementations existed.

The interface defined the following methods:

  • usort($callbackFunction) – for sorting a collection via a given callback function
  • moveItemAt($currentPosition, $newPosition = 0) – for moving items within the collection

Impact 

Any code that implements or references \SortableCollectionInterface will trigger a PHP fatal error.

Since this interface was never implemented in the TYPO3 Core and had no real-world usage, the impact should be minimal for most installations.

Affected installations 

Installations with custom extensions that implement or reference the \SortableCollectionInterface are affected.

Migration 

Remove any references to \SortableCollectionInterface from your code.

If you need sortable collection functionality, implement your own sorting logic directly in your collection classes, or use PHP's built-in array sorting functions such as usort(), uasort(), or uksort().

Breaking: #68303 - Make tt_content imagewidth/imageheight nullable 

See forge#68303

Description 

The default values of the fields imagewidth and imageheight in the tt_content table are now set to null.

This change removes the awkward UI behavior where the fields were previously set to 0 when no value was entered.

Impact 

Custom queries might fail if they expect the fields to be 0 instead of null.

Affected installations 

TYPO3 installations that rely on the imagewidth and imageheight fields of the tt_content table always being integers are affected.

Migration 

Use the "Media fields zero to null" upgrade wizard to update existing field values.

Also, modify your queries to handle null values instead of 0.

Breaking: #85323 - Move GET parameters in sitemap into namespace 

See forge#85323

Description 

The names of the GET parameters used in the sitemap generated by EXT:seo have been changed from page and sitemap to tx_seo[page] and tx_seo[sitemap], respectively.

Impact 

Applications, routing configurations, and third-party tools that rely on the parameters being named page and sitemap will break.

Affected installations 

This affects installations that use the arguments page and sitemap or override the sitemap templates of EXT:seo.

Migration 

If the arguments are mapped in the routing configuration, the code needs to be slightly adapted. A working example:

routeEnhancers:
  Sitemap:
    type: Simple
    routePath: 'sitemap-type/{sitemap}'
    aspects:
      sitemap:
        type: StaticValueMapper
        map:
          pages: pages
          tx_news: tx_news
          my_other_sitemap: my_other_sitemap
    _arguments:
      sitemap: 'tx_seo/sitemap'
Copied!

If the templates in EXT:seo/Resources/Private/Templates/XmlSitemap/Index.xml have been modified, adjust the generated links to match the original ones.

If the URL to a single sitemap has been provided to a third-party tool such as a crawler or search engine, it must be re-added using the new URL.

Breaking: #92434 - Use Record API in Page Module Preview Rendering 

See forge#92434

Description 

The Page Module preview rendering has been refactored to use the Record API internally instead of accessing raw database arrays. This affects both custom preview renderers that extend StandardContentPreviewRenderer and Fluid-based preview templates.

The method signature has changed for \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer :

  • StandardContentPreviewRenderer->linkEditContent() now expects a RecordInterface object as the second $record parameter instead of an array

The \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent has also been updated:

  • PageContentPreviewRenderingEvent->getRecord() now returns a RecordInterface object instead of an array
  • PageContentPreviewRenderingEvent->setRecord() now expects a RecordInterface object instead of an array

Additionally, the @internal class \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem has been updated to work with Record objects:

  • The constructor of GridColumnItem now requires a RecordInterface object as the third $record parameter instead of an array
  • GridColumnItem->getRecord() now returns a RecordInterface object instead of an array
  • GridColumnItem->setRecord() now expects a RecordInterface object instead of an array
  • A new method GridColumnItem->getRow() has been added to access the raw database array if needed

For Fluid-based content element previews, the template variables have changed. Previously, all record fields were passed as individual variables to the Fluid template. Now, only a single {record} variable is passed, which is a RecordInterface object providing access to all record data through the Record API.

Using the {pi_flexform_transformed} variable in Fluid-based content element previews no longer works. The resolved flex form can be directly accessed on the RecordInterface object, for example via {record.pi_flexform}. The value is a FlexFormFieldValues object, which properly groups the fields by their sheets.

Impact 

Extensions that extend StandardContentPreviewRenderer and override the linkEditContent() method will need to update their method signature.

Extensions that access GridColumnItem->getRecord() expecting an array will need to update their code to work with RecordInterface objects.

Extensions using event listeners for PageContentPreviewRenderingEvent that access the record via getRecord() expecting an array will need to update their code to work with RecordInterface objects.

Custom Fluid templates for content element preview rendering must be updated to use the {record} variable instead of accessing individual field variables.

Affected installations 

All installations with extensions that:

  • Extend StandardContentPreviewRenderer and call or override the linkEditContent() method
  • Instantiate GridColumnItem or call GridColumnItem->getRecord() / GridColumnItem->setRecord()
  • Register event listeners for PageContentPreviewRenderingEvent
  • Use custom Fluid templates for content element preview rendering via PageTSconfig mod.web_layout.tt_content.preview.[recordType]

Migration 

For custom preview renderers extending StandardContentPreviewRenderer :

Update the method signature of linkEditContent() to accept a RecordInterface object:

Before (TYPO3 v13 and lower)
protected function linkEditContent(string $linkText, array $row, string $table = 'tt_content'): string
{
    $uid = (int)$row['uid'];
    $pid = (int)$row['pid'];
    // ...
}
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Core\Domain\RecordInterface;

protected function linkEditContent(string $linkText, RecordInterface $record): string
{
    $uid = $record->getUid();
    $pid = $record->getPid();
    $table = $record->getMainType();
    // ...
}
Copied!

For code working with GridColumnItem :

Before (TYPO3 v13 and lower)
$row = $columnItem->getRecord();
$uid = (int)$row['uid'];
$title = $row['header'];
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Core\Domain\RecordInterface;

$record = $columnItem->getRecord();
$uid = $record->getUid();
$title = $record->has('header') ? $record->get('header') : '';

// Or if raw array access is needed:
$row = $columnItem->getRow();
$uid = (int)$row['uid'];
Copied!

For custom Fluid templates used for content element preview rendering:

Before (TYPO3 v13 and lower)
<h2>{header}</h2>
<p>{bodytext}</p>
<f:if condition="{image}">
    <p>Image UID: {image}</p>
</f:if>
Copied!
After (TYPO3 v14+)
<h2>{record.header}</h2>
<p>{record.bodytext}</p>
<f:if condition="{record.image}">
    <p>Image UID: {record.image.uid}</p>
</f:if>
Copied!

For flex form value rendering, there are two options:

Before (TYPO3 v13 and lower)
<h2>{header}</h2>
<p>{bodytext}</p>
<small>{pi_flexform_transformed.settings.welcome_header}</small>
<f:if condition="{image}">
    <p>Image UID: {image}</p>
</f:if>
Copied!
After (TYPO3 v14+)
<f:variable name="path" value="s_messages/settings" />
<small>{record.pi_flexform.{path}.welcome_header}</small>

<!-- or -->

<small>{record.pi_flexform.sheets.s_messages.settings.welcome_header}</small>
Copied!

Breaking: #97151 - Remove "Database Relations" backend module 

See forge#97151

Description 

The backend submodule Database Relations within DB Check provided information about potentially broken database relations. However, the information it displayed was very limited and barely helpful. In addition, the entire module and its code have not received any meaningful updates in recent years.

Due to this, the module has been removed.

Impact 

The module has been removed. Existing links and stored bookmarks will no longer work.

Affected installations 

All TYPO3 installations are affected.

Migration 

There is no migration available.

Breaking: #98070 - Remove eval method year 

See forge#98070

Description 

The eval method year was used to validate the value of a TCA field. Its implementation was never completed and simply cast the value to an integer.

As there is no clear definition of what a valid year value should be, the method has been removed without substitution.

Impact 

The value year has been removed from the list of supported eval options.

The TCA migration will trigger a deprecation log entry when building the final TCA.

Affected installations 

TYPO3 installations using old extensions that define custom TCA configurations with this option set are affected.

Migration 

Remove the year eval setting from your TCA configuration and use a TCA field type that better suits your needs.

// Use type "number" with an optional range restriction
'variant_a' => [
    'label' => 'My year',
    'config' => [
        'type' => 'number',
        'range' => [
            'lower' => 1990,
            'upper' => 2038,
        ],
        'default' => 0,
    ],
],

// Use a date field with an optional range restriction
'variant_b' => [
    'label' => 'My year',
    'config' => [
        'type' => 'datetime',
        'range' => [
            'lower' => gmmktime(0, 0, 0, 1, 1, 1990),
            'upper' => gmmktime(23, 59, 59, 12, 31, 2038),
        ],
        'nullable' => true,
    ],
],
Copied!

Breaking: #98239 - Removed "afterBuildingFinished" hook 

See forge#98239

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'] has been removed in favor of the more powerful PSR-14 events \TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent and \TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent .

Impact 

Any hook implementation registered under this identifier will no longer be executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions that implement this hook are affected. The extension scanner reports such usages as a weak match.

Migration 

The hook has been removed without a deprecation phase to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and v14+ (using the new events). Implementing the PSR-14 events provides the same or greater control over form rendering.

Use the BeforeRenderableIsAddedToFormEvent or AfterFormIsBuiltEvent to achieve the same functionality with the new event-based system.

Breaking: #101292 - Strong-typed PropertyMappingConfigurationInterface 

See forge#101292

Description 

Extbase's PropertyMappingConfigurationInterface is now fully typed with native PHP types.

Impact 

Existing implementations will no longer work without adjustment. According to the Liskov Substitution Principle, all implementations must follow the updated method signatures and type restrictions defined in the interface.

Affected installations 

TYPO3 installations with custom PHP code implementing a custom PropertyMappingConfiguration are affected. Such cases are rare.

Migration 

Add the required native PHP types to all custom implementations of the PropertyMappingConfigurationInterface to fulfill the updated interface definition.

Breaking: #101392 - getIdentifier() and setIdentifier() from AbstractFile removed 

See forge#101392

Description 

When using the PHP API of the File Abstraction Layer (FAL), several classes are involved in representing file objects.

In addition to the FileInterface , there is also the AbstractFile class, from which most file- related classes inherit.

To ensure stricter type consistency, the abstract class no longer implements the methods getIdentifier() and setIdentifier(). Implementing these methods is now the responsibility of each subclass.

The methods are now implemented in the respective concrete classes inheriting from AbstractFile .

Impact 

In the unlikely case that the TYPO3 File Abstraction Layer has been extended with custom PHP classes derived from AbstractFile , this change will cause a fatal PHP error, as the new abstract methods getIdentifier() and setIdentifier() must be implemented by the subclass.

Affected installations 

TYPO3 installations that include custom code extending the File Abstraction Layer are affected. Such cases are considered highly uncommon.

Migration 

Implement the two methods getIdentifier() and setIdentifier() in any custom file class extending AbstractFile .

This can also be implemented in older TYPO3 versions to ensure forward compatibility with TYPO3 v14 and later.

Breaking: #103141 - Use doctrine GUID type for TCA type=uuid 

See forge#103141

Description 

The TYPO3 Doctrine implementation can now handle the proper matching database type for UUID's.

Postgresql natively supports the UUID data type and is way faster than the prior VARCHAR(36) generated from the string type.

The Doctrine DBAL GUID type uses CHAR(26) as the fixed field column size for non-postgres databases, which is compatible as long as valid UUID values were persisted in the configured database table.

Impact 

TYPO3 database tables can now natively properly apply the suitable GUID column type when configured as TCA type=uuid.

A prerequisite for this is that you only have valid UUID's stored in the database table, otherwise the database update will report an error when applying migrations.

Affected installations 

Projects with database table columns set as TCA type=uuid.

Error are likely to occur, if invalid UUID data is stored in field columns configured with this type.

Migration 

Use the database analyzer to migrate the database fields. Invalid values are not migrated and need to be manually cleaned up in affected instances.

Breaking: #103910 - Change logout handling in EXT:felogin 

See forge#103910

Description 

The logout handling has been adjusted to correctly dispatch the PSR-14 event \TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent when a logout redirect is configured. The actionUri variable has been removed, and the logout template has been updated to reflect this change, including correct use of the noredirect functionality.

Impact 

The PSR-14 event LogoutConfirmedEvent is now correctly dispatched when a logout redirect is configured. Additionally, the noredirect parameter is now evaluated during logout.

Affected installations 

TYPO3 installations using EXT:felogin with a custom Fluid template for the logout form.

Migration 

The {actionUri} variable is no longer available and must be removed from custom templates.

Before:

Fluid template adjustment (before)
<!-- Before -->
<f:form action="login" actionUri="{actionUri}" target="_top" fieldNamePrefix="">
Copied!

After:

Fluid template adjustment (after)
<f:form action="login" target="_top" fieldNamePrefix="">
Copied!

The evaluation of the {noRedirect} variable must be added to the template:

Before:

Fluid template adjustment for noRedirect (before)
<div class="felogin-hidden">
    <f:form.hidden name="logintype" value="logout" />
</div>
Copied!

After:

Fluid template adjustment for noRedirect (after)
<div class="felogin-hidden">
    <f:form.hidden name="logintype" value="logout" />
    <f:if condition="{noRedirect} != ''">
        <f:form.hidden name="noredirect" value="1" />
    </f:if>
</div>
Copied!

Breaking: #103913 - Do not perform redirect in EXT:felogin logoutAction 

See forge#103913

Description 

Redirect handling for the logoutAction has been removed.

Impact 

The logoutAction no longer performs any configured redirect via plugin settings or GET parameters.

Affected installations 

TYPO3 installations relying on redirect handling within logoutAction are affected.

Migration 

No migration is required. The previous redirect logic in logoutAction() has been removed because it was incorrect: it ignored the showLogoutFormAfterLogin setting and could trigger an unintended redirect even when this option was enabled.

Valid redirects are already processed correctly by loginAction() and overviewAction(), so the faulty branch was removed without replacement.

Breaking: #105377 - Deprecated functionality removed 

See forge#105377

Description 

The following PHP classes that have previously been marked as deprecated with v13 have been removed:

The following PHP classes have been declared final:

The following PHP interfaces that have previously been marked as deprecated with v13 have been removed:

The following PHP interfaces changed:

  • \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->modifyView() added (Deprecation entry)
  • \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->render() removed (Deprecation entry)
  • \TYPO3\CMS\Core\PageTitle\PageTitleProviderInterface->setRequest() added forge#102817

The following PHP class aliases that have previously been marked as deprecated with v13 have been removed:

The following PHP class methods that have previously been marked as deprecated with v13 have been removed:

The following PHP static class methods that have previously been marked as deprecated for v13 have been removed:

The following methods changed signature according to previous deprecations in v13 at the end of the argument list:

  • \TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->__construct() - All arguments are now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Imaging\IconFactory->getIcon() (argument 4 is now of type \TYPO3\CMS\Core\Imaging\IconState|null) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\FileInterface->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\FileReference->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->addFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->addUploadedFile() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->addFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->addUploadedFile() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->addFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->addUploadedFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->copyFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->copyFolder() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->moveFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->moveFolder() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->renameFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() (argument 2 $type and 3 $extensionKey have been dropped) (Deprecation entry)

The following public class properties have been dropped:

The following class property has changed/enforced type:

  • \TYPO3\CMS\Extbase\Mvc\Controller\ActionController->view (is now \TYPO3\CMS\Core\View\ViewInterface ) (Deprecation entry)

The following TypoScript options have been dropped or adapted:

The following user TSconfig options have been removed:

The following class constants have been dropped:

The following global option handling have been dropped and are ignored:

The following global variables have been changed:

  • $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['driver-middleware-identifier'] must be an array, not a class string (Deprecation entry)

The following hooks have been removed:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] (Deprecation entry)

The following TCA options are not evaluated anymore:

  • $GLOBALS['TCA'][$table]['types']['subtype_value_field']
  • $GLOBALS['TCA'][$table]['types']['subtypes_addlist']
  • $GLOBALS['TCA'][$table]['types']['subtypes_excludelist']

The following extbase validator options have been removed:

The following fallbacks have been removed:

  • Accepting arrays returned by readFileContent() in Indexed Search external parsers (Deprecation entry)
  • Allowing instantiation of \TYPO3\CMS\Core\Imaging\IconRegistry in ext_localconf.php (Deprecation entry)
  • Accepting a comma-separated list of fields as value for the columnsOnly parameter (Deprecation entry)
  • Support for extbase repository magic findByX(), findOneByX() and countByX() methods (Deprecation entry)
  • Fluid view helpers that extend \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper should no longer register class attribute and should rely on attribute auto registration for the error class to be added correctly. (Deprecation entry)
  • The legacy backend entry point typo3/index.php has been removed along with handling of composer.json setting extra.typo3/cms.install-deprecated-typo3-index-php (Deprecation entry)

The following upgrade wizards have been removed:

  • Install extension "fe_login_mode" from TER
  • Migrate base and path to the new identifier property of the "sys_filemounts" table
  • Migrate site settings to separate file
  • Set workspace records in table "sys_template" to deleted
  • Migrate backend user and groups to new module names
  • Migrate backend groups "explicit_allowdeny" field to simplified format
  • Migrate sys_log entries to a JSON formatted value
  • Migrate storage and folder to the new folder_identifier property of the "sys_file_collection" table

The following row updater has been removed:

  • \TYPO3\CMS\Install\Updates\RowUpdater\SysRedirectRootPageMoveMigration

The following database table fields have been removed:

The following JavaScript modules have been removed:

The following JavaScript method behaviours have changed:

  • FormEngineValidation.markFieldAsChanged() always requires HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement to be passed as first argument (Deprecation entry)
  • FormEngineValidation.validateField() always requires HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement to be passed as first argument (Deprecation entry)

The following JavaScript method has been removed:

The following smooth migration for JavaScript modules have been removed:

  • @typo3/backend/page-tree/page-tree-element to @typo3/backend/tree/page-tree-element (Deprecation entry)

The following localization XLIFF files have been removed:

The following template files have been removed:

The following content element definitions have been removed:

Impact 

Using above removed functionality will most likely raise PHP fatal level errors, may change website output or crashes browser JavaScript.

Breaking: #105549 - Improved ISO8601 Date Handling in TYPO3 DataHandler 

See forge#105549

Description 

The DataHandler PHP API has been extended to support both qualified and unqualified ISO8601 date formats, in order to correctly process supplied timezone offsets when provided.

  • Qualified ISO8601: Includes an explicit timezone offset (for example, 1999-12-11T10:09:00+01:00 or 1999-12-11T10:09:00Z)
  • Unqualified ISO8601: Omits timezone offsets, representing LOCALTIME (for example, 1999-12-11T10:09:00)

Previously, TYPO3 incorrectly used qualified ISO8601 with Z (UTC+00:00) to denote LOCALTIME and applied the server's timezone offset, which led to misinterpretations when another timezone offset was provided, or when real UTC-0 was intended instead of LOCALTIME. Now, timezone offsets are accurately applied if supplied, and are based on server localtime if omitted.

TYPO3 will use unqualified ISO8601 dates internally for communication between FormEngine and the DataHandler API, ensuring timezone offsets are correctly processed – instead of being shifted – when supplied to the DataHandler API.

In essence, this means that existing workarounds for previously applied timezone offsets should be reviewed and removed.

Impact 

TYPO3 now provides accurate and consistent handling of ISO8601 dates, eliminating previous issues related to timezone interpretation and LOCALTIME representation.

Affected installations 

Installations with custom TYPO3 extensions that invoke the DataHandler API with data for type="datetime" fields are affected.

Migration 

Qualified ISO8601 dates with intended timezone offsets and \DateTimeInterface objects can now be passed directly to the DataHandler without requiring manual timezone adjustments.

An example of a previous workaround that added timezone offsets for the DataHandler:

Passing datetime data via DataHandler PHP API (before)
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // A previous workaround added the localtime offset to supplied
            // dates, as it was subtracted by the DataHandler persistence
            // layer
            'mydatefield_1' => gmdate('c', $myDate->getTimestamp() + (int)date('Z')),
        ],
    ],
]);
Copied!

Previous timezone-shifting workarounds can be removed and replaced with more intuitive formats.

Passing datetime data via DataHandler PHP API (after)
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // Pass \DateTimeInterface object directly
            'mydatefield_1' => $myDate,
            // Format as LOCALTIME
            'mydatefield_2' => $myDate->format('Y-m-d\TH:i:s'),
            // Format with timezone information
            // (offsets will be normalized to the persistence timezone
            // format: UTC for integer fields, LOCALTIME for native
            // DATETIME fields)
            'mydatefield_3' => $myDate->format('c'),
        ],
    ],
]);
Copied!

Breaking: #105686 - Avoid obsolete $charset in sanitizeFileName() 

See forge#105686

Description 

The interface \TYPO3\CMS\Core\Resource\Driver\DriverInterface has been updated.

The method signature

public function sanitizeFileName(string $fileName, string $charset = ''): string
Copied!

has been simplified to:

public function sanitizeFileName(string $fileName): string
Copied!

Implementing classes no longer need to handle a second argument.

Impact 

This change has little to no impact, since the main API caller - the Core class ResourceStorage - never passed a second argument. The default implementation, LocalDriver , has therefore always behaved as if handling UTF-8 strings.

Affected installations 

TYPO3 installations with custom File Abstraction Layer (FAL) drivers implementing DriverInterface may be affected.

Migration 

Implementing classes should drop support for the second argument. Retaining it does not cause a conflict with the interface, but the TYPO3 Core will never call sanitizeFileName() with a second parameter.

Breaking: #105695 - Simplified CharsetConverter 

See forge#105695

Description 

The following methods have been removed from \TYPO3\CMS\Core\Charset\CharsetConverter :

  • CharsetConverter->conv()
  • CharsetConverter->utf8_encode()
  • CharsetConverter->utf8_decode()
  • CharsetConverter->specCharsToASCII(), use CharsetConverter->utf8_char_mapping() instead
  • CharsetConverter->sb_char_mapping()
  • CharsetConverter->euc_char_mapping()

This removes most helper methods that implemented conversions between different character sets from the TYPO3 Core. The vast majority of websites now use UTF-8 and no longer require the expensive charset conversions previously provided by the Core framework.

Impact 

Calling any of the removed methods will trigger a fatal PHP error.

Affected installations 

The TYPO3 Core has not exposed any of this low-level functionality in upper layers such as TypoScript for quite some time. The removal should therefore have little to no impact on most installations.

The only cases that may be affected are import or export extensions that perform conversions between legacy character sets (for example, those in the EUC family). Affected extensions can mitigate this change by copying the TYPO3 v13 version of the class CharsetConverter , including the relevant files from core/Resources/Private/Charsets/csconvtbl/, into their own codebase.

The extension scanner will detect usages and classify them as weak matches.

Migration 

Avoid calling any of the removed methods. Extensions that still require this functionality should copy the necessary logic into their own codebase or use a third-party library.

This particular case has a direct substitution:

// Before
$charsetConverter->specCharsToASCII('utf-8', $myString);

// After
$charsetConverter->utf8_char_mapping($myString);
Copied!

Breaking: #105728 - Extbase backend modules not in page context rely on global TypoScript only 

See forge#105728

Description 

Configuration of Extbase-based backend modules can be done using frontend TypoScript.

The standard prefix in TypoScript to do this is module.tx_myextension. Extbase backend module controllers can typically retrieve their configuration using a call like: $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'myextension');

TypoScript itself is always bound to a page: The frontend must have either some rootline page with a sys_template record or a page that has a site

set, otherwise frontend rendering will terminate with an error message.

Extbase-based backend modules are sometimes bound to pages as well: They can have a rendered page tree configured in their module configuration and then receive the selected page UID within the request as GET parameter id.

Other Extbase-based backend modules, however, are not inside a page scope and do not render the page tree. Examples of such modules within the TYPO3 Core are the backend modules delivered by the form and beuser extensions.

Such Extbase-based backend modules without a page tree had a hard time calculating their relevant frontend TypoScript-based configuration: Since TypoScript is bound to pages, they looked for "the first" valid page in the page tree, and the first valid sys_template record to calculate their TypoScript configuration. This dependency on guesswork made final configuration of Extbase backend module configuration not in page context brittle, opaque, and clumsy.

TYPO3 v14 puts an end to this: Extbase backend modules without page context compile their TypoScript configuration from global TypoScript only and no longer calculating TypoScript by guessing "the first valid" page.

The key call to register such "global" TypoScript is the method ExtensionManagementUtility::addTypoScriptSetup() in ext_localconf.php files.

Impact 

Configuration of Extbase-based backend modules may change if their configuration is defined by the first valid page in the page tree. Configuration of such backend modules can no longer be changed by including TypoScript on the "first valid" page.

Affected installations 

Instances with Extbase-based backend modules without a page tree may be affected.

Migration 

Configuration of Extbase-based backend modules without a page tree must be supplied programmatically and made "global" by extending $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] using ExtensionManagementUtility::addTypoScriptSetup() within extensions’ ext_localconf.php files. The backend module of the form extension is a good example. Additional locations of extensions that deliver form YAML definitions are defined like this:

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTypoScriptSetup('
    module.tx_form {
        settings {
            yamlConfigurations {
                1732884807 = EXT:my_extension/Configuration/Yaml/FormSetup.yaml
            }
        }
    }
');
Copied!

Note it is also possible to use the method ExtensionManagementUtility::addTypoScriptConstants() to declare "global" TypoScript constants and to use them in the TypoScript shown above.

Breaking: #105733 - FileNameValidator no longer accepts custom regex in __construct() 

See forge#105733

Description 

The class FileNameValidator no longer accepts a custom file deny pattern in __construct(). The service is now stateless and can be injected without side effects.

Impact 

A custom partial regex passed as the first constructor argument when instantiating the service is now ignored. The service relies on $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] configuration and a hard-coded constant as a fallback.

Affected installations 

Instances with custom extensions using GeneralUtility::makeInstance(FileNameValidator::class, 'some-custom-pattern'); are affected. This is expected to be a very rare case.

Migration 

Extensions that need to be tested with custom patterns that cannot be declared globally using $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] should implement their own service for this purpose or inline the necessary code. The core implementation performing the check is only a few lines long.

Breaking: #105809 - AfterMailerInitializationEvent removed 

See forge#105809

Description 

The event \TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent has been removed. This event became obsolete with the introduction of the Symfony-based mailer in TYPO3 v10. It was only able to influence the TYPO3 Core mailer by calling the @internal method injectMailSettings() after the settings had already been determined. The event has been removed since it no longer had a meaningful use case.

Impact 

Event listeners registered for this event will no longer be triggered.

Affected installations 

This event has had little purpose since the switch to the Symfony-based mailer and is probably not used in most instances. The extension scanner will find usages.

Migration 

Check if this event can be substituted by reconfiguring $GLOBALS['TYPO3_CONF_VARS']['MAIL'] , or by listening for the event BeforeMailerSentMessageEvent instead.

Breaking: #105863 - Remove exposeNonexistentUserInForgotPasswordDialog setting in EXT:felogin 

See forge#105863

Description 

The TypoScript setting exposeNonexistentUserInForgotPasswordDialog has been removed in EXT:felogin.

Impact 

Using the TypoScript setting exposeNonexistentUserInForgotPasswordDialog has no effect anymore. The password recovery process in EXT:felogin now always shows the same message when a username or email address is submitted in the password recovery form.

Affected installations 

Websites using the TypoScript setting exposeNonexistentUserInForgotPasswordDialog in EXT:felogin are affected.

Migration 

The setting has been removed without replacement. It is possible to use the PSR-14 event SendRecoveryEmailEvent to implement similar functionality if absolutely necessary. From a security perspective, however, it is strongly recommended not to expose the existence of email addresses or usernames.

Breaking: #105920 - Folder->getSubFolder() throws FolderDoesNotExistException 

See forge#105920

Description 

An exception handling detail within the File Abstraction Layer (FAL) resource handling has been changed. When calling getSubFolder('mySubFolderName') on a Folder object, and if this subfolder does not exist, the specific \TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException is now raised instead of the global \InvalidArgumentException.

Impact 

The change may affect extensions that directly or indirectly call Folder->getSubFolder() and expect a \InvalidArgumentException to be thrown.

Affected installations 

FolderDoesNotExistException does not extend \InvalidArgumentException. Code that currently expects a \InvalidArgumentException to be thrown needs to be adapted.

Migration 

The change is breaking for code that takes an "optimistic" approach like: "get the subfolder object, and if this throws, create one". Example:

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (\InvalidArgumentException) {
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

This should be changed to catch a FolderDoesNotExistException instead:

use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (FolderDoesNotExistException) {
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

Extensions that need to stay compatible with both TYPO3 v13 and v14 should catch both exceptions and should later avoid catching \InvalidArgumentException when v13 compatibility is dropped:

use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (\InvalidArgumentException|FolderDoesNotExistException) {
    // @todo: Remove \InvalidArgumentException from catch list when
    //        TYPO3 v13 compatibility is dropped.
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

Breaking: #106041 - TypoScript Extbase toggle config.tx_extbase.persistence.updateReferenceIndex removed 

See forge#106041

Description 

Extbase previously supported the TypoScript toggle config.tx_extbase.persistence.updateReferenceIndex to control whether the reference index should be updated when records are persisted.

It has become increasingly important that the reference index is always kept up to date, since an increasing number of TYPO3 Core components rely on current reference index data. Using the reference index at key points can improve read and rendering performance significantly.

This toggle has been removed. Reference index updating is now always enabled.

Impact 

The change may slightly increase database load, which can become noticeable when Extbase updates many records at once.

Affected installations 

Instances with extensions that write many records using the Extbase persistence layer may be affected.

Migration 

The TypoScript toggle config.tx_extbase.persistence.updateReferenceIndex should be removed from any extension codebase, as it is now ignored by Extbase.

Breaking: #106056 - Add setRequest and getRequest to Extbase ValidatorInterface 

See forge#106056

Description 

Custom validators implementing \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface must now also implement the methods setRequest() and getRequest().

Impact 

Missing implementations of the methods setRequest() and getRequest() will now result in a PHP fatal error.

Affected installations 

TYPO3 installations with custom extensions implementing ValidatorInterface .

Migration 

The methods setRequest() and getRequest() must be implemented in affected validators.

If there is no need to directly implement ValidatorInterface , it is recommended to extend AbstractValidator , where both methods are already implemented.

Breaking: #106118 - Property DataHandler->storeLogMessages removed 

See forge#106118

Description 

The public property \TYPO3\CMS\Core\DataHandling\DataHandler->storeLogMessages has been removed without substitution. It should no longer be used by extensions.

Impact 

Setting or reading the property in an extension will now raise a PHP warning- level error.

Affected installations 

Instances with extensions that access this property are affected. This should be a very rare use case. No TYPO3 Extension Repository (TER) extensions were affected during verification. The extension scanner is configured to find usages as a weak match.

Migration 

The property has been removed. Any code setting or reading it from \TYPO3\CMS\Core\DataHandling\DataHandler instances should be removed. The DataHandler->log() method now always writes the given $details to the sys_log table.

Breaking: #106307 - Use stronger cryptographic algorithm for HMAC 

See forge#106307

Description 

TYPO3 now uses SHA3-256 for HMAC operations across multiple components, replacing the previously used MD5, SHA1, and SHA256 algorithms. SHA3-256 (Keccak) produces 64-character hexadecimal hashes compared to the 32 or 40 characters of the older algorithms.

The following components have been upgraded:

Component Previous HMAC New HMAC
cHash MD5 SHA3-256
Backend password recovery SHA1 SHA3-256
Frontend password recovery SHA1 SHA3-256
File dump controller SHA1 SHA3-256
Show image controller SHA1 SHA3-256
Backend form protection SHA1 SHA3-256
Extbase form request attributes SHA1 SHA3-256
Form extension request attributes SHA1 SHA3-256
Database session backend SHA256 SHA3-256
Redis session backend SHA256 SHA3-256

Database fields have been extended to accommodate the longer hash values (and would even support SHA3-512 with 128 hexadecimal characters in the future):

  • be_users.password_reset_token: 100 → 128 characters
  • fe_users.felogin_forgotHash: 80 → 160 characters (including additional timestamp details)

Impact 

The algorithm change has the following immediate effects:

URLs with HMAC tokens become invalid:

  • cHash parameters in frontend URLs are invalidated
  • File dump URLs (file downloads) require regeneration
  • Show image URLs require regeneration

Active password reset tokens expire:

  • Backend user password reset links in progress become invalid
  • Frontend user password reset links in progress become invalid
  • Users must request new password reset emails

Session handling:

  • Existing session identifiers will be regenerated on next user login
  • No immediate session invalidation occurs

Database schema:

  • Field lengths are automatically updated during upgrade
  • No data migration is required for existing records

Affected installations 

All installations upgrading to TYPO3 v14 are affected.

The impact varies based on usage:

  • High impact: installations with active password reset processes or cached frontend URLs with cHash parameters
  • Medium impact: installations using file dump or show image controllers with externally stored URLs
  • Low impact: all other installations (automatic migration on next use)

Migration 

Database schema updates:

Execute the database analyzer in the Install Tool or run vendor/bin/typo3 upgrade:run.

URLs and caching:

  • Frontend caches should be cleared to regenerate cHash values
  • File dump and show image URLs regenerate automatically on next access
  • External references to file or image URLs must be updated

Sessions:

No manual intervention is required. Sessions are automatically rehashed on next login.

Custom extensions:

If custom code uses HashService::hmac() directly, review whether the default SHA1 algorithm is still appropriate. Consider explicitly passing HashAlgo::SHA3_256 for new HMAC operations:

use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;

$hash = $hashService->hmac($data, 'my-additional-secret', HashAlgo::SHA3_256);
Copied!

Breaking: #106405 - TypolinkBuilder signature changes 

See forge#106405

Description 

To enable dependency injection for TypolinkBuilder classes, several breaking changes were introduced to the TypolinkBuilder architecture.

The following breaking changes have been made:

  • The constructor of AbstractTypolinkBuilder has been removed. Extending classes can no longer rely on receiving ContentObjectRenderer and \TypoScriptFrontendController through the constructor.
  • All concrete TypolinkBuilder implementations now implement the new TypolinkBuilderInterface and use dependency injection via their constructors instead of extending AbstractTypolinkBuilder with constructor arguments.
  • The method signature of the main link-building method has changed from build(array &$linkDetails, string $linkText, string $target, array $conf) to buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = '').

Impact 

Custom TypolinkBuilder implementations extending AbstractTypolinkBuilder will fail with fatal errors due to the removed constructor and changed method signatures.

Extensions that instantiate TypolinkBuilder classes directly will also fail, as the constructor signatures have fundamentally changed to use dependency injection.

Affected installations 

TYPO3 installations with extensions that:

  • Create custom TypolinkBuilder classes extending AbstractTypolinkBuilder
  • Directly instantiate TypolinkBuilder classes in PHP code
  • Override or extend the build() method of TypolinkBuilder classes

Migration 

For custom TypolinkBuilder implementations:

  1. Implement TypolinkBuilderInterface
  2. Use dependency injection in the constructor for required services
  3. Replace the build() method with buildLink()

Note: Classes implementing TypolinkBuilderInterface are automatically configured as public services in the dependency injection container - no manual configuration is required.

Example migration:

Before (TYPO3 v13 and lower)
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;

class MyCustomLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
    {
        // Custom link building logic
        return new LinkResult('news', $linkText);
    }
}
Copied!
After (TYPO3 v14+)
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function __construct(
        // Inject required dependencies
    ) {}

    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = '',
    ): LinkResultInterface {
        // Custom link building logic - access ContentObjectRenderer via:
        $contentObjectRenderer = $request->getAttribute('currentContentObject');
        return new LinkResult('news', $linkText);
    }
}
Copied!

For code that instantiates TypolinkBuilder classes directly:

It is strongly recommended to use the LinkFactory instead of instantiating TypolinkBuilder classes directly. The LinkFactory handles proper instantiation and dependency injection automatically.

Breaking: #106412 - TCA interface settings for list view removed 

See forge#106412

Description 

Each TCA definition previously had an optional section named ['interface'], which defined parameters for displaying TCA records.

The last remaining options within this section, $GLOBALS['TCA'][$table]['interface']['maxSingleDBListItems'] and $GLOBALS['TCA'][$table]['interface']['maxDBListItems'] , have been removed. As a result, the entire ['interface'] section is no longer supported and will be ignored.

These settings were used to define the number of table rows displayed within the Content > List backend module.

Impact 

The $GLOBALS['TCA'][$table]['interface'] section in TCA definitions is no longer evaluated.

Setting any values under this key in custom extensions has no effect and will be automatically removed during build time.

Affected installations 

TYPO3 installations with custom TCA settings defining $GLOBALS['TCA'][$table]['interface'] are affected.

Migration 

Visual display settings can still be overridden on a per-user or per-page basis via TSconfig. This approach is more flexible, as it allows rendering different numbers of items per site, page tree, or user group.

The TCA option $GLOBALS['TCA'][$table]['interface']['maxSingleDBListItems'] has been removed in favor of mod.web_list.itemsLimitSingleTable.

The TCA option $GLOBALS['TCA'][$table]['interface']['maxDBListItems'] has been removed in favor of mod.web_list.itemsLimitPerTable.

Breaking: #106503 - Removal of fields from sys_file_metadata 

See forge#106503

Description 

The following database fields and corresponding TCA definitions have been removed from the database table sys_file_metadata without substitution:

  • visible
  • fe_groups

These fields were added to the table when the system extension EXT:filemetadata was installed.

Although the field names suggested access control functionality similar to those found in other TYPO3 tables, they were never configured to restrict frontend or backend access. Their intended meaning would have depended on custom implementation and was not supported by the TYPO3 Core.

Additionally, the field status has been moved from the Access tab to the Metadata tab to avoid the impression that this field has any restrictive behavior.

Impact 

The fields visible and fe_groups in sys_file_metadata are no longer used. After performing a Database Compare in the Install Tool, these columns will be removed and their data permanently lost.

Accessing these fields in PHP or TypoScript will result in PHP warnings.

Affected installations 

Any TYPO3 installation using the fields visible or fe_groups provided by the system extension EXT:filemetadata is affected.

Migration 

No automatic migration is available.

If the fields are still required for custom logic, reintroduce their database columns and TCA configuration within a custom extension.

Breaking: #106596 - Remove legacy form templates 

See forge#106596

Description 

In earlier TYPO3 versions, the Form Framework provided two template variants for frontend rendering:

  • The initial, legacy templates in EXT:form/Resources/Private/Frontend/Templates and EXT:form/Resources/Private/Frontend/Partials, which were deprecated in forge#95456
  • The newer, Bootstrap 5 compatible and accessible templates in EXT:form/Resources/Private/FrontendVersion2/Templates and EXT:form/Resources/Private/FrontendVersion2/Partials, introduced with forge#94868

The legacy form templates have now been removed. The rendering option templateVariant, which toggled the template and form configuration variant, has been removed as well.

The newer template variants have been moved to the original file paths of the legacy templates.

Impact 

The removal of the legacy templates and the templateVariant configuration option simplifies the Form Framework rendering logic.

Developers no longer need to choose between multiple template variants, reducing complexity and improving maintainability. Projects already using the newer templates benefit from a cleaner configuration and a unified rendering approach.

Affected installations 

All TYPO3 installations using the Form Framework are affected.

Migration 

If you still rely on the legacy templates, you must migrate your templates and partials to the structure of the newer templates.

Websites that use templateVariant: version2 can simplify their form configuration. Variants with the condition 'getRootFormProperty("renderingOptions.templateVariant") == "version2"' are no longer necessary and can be removed.

Before:

prototypes:
  standard:
    formElementsDefinition:
      Text:
        variants:
          -
            identifier: template-variant
            condition: 'getRootFormProperty("renderingOptions.templateVariant") == "version2"'
            properties:
              containerClassAttribute: 'form-element form-element-text mb-3'
              elementClassAttribute: form-control
              elementErrorClassAttribute: ~
              labelClassAttribute: form-label
Copied!

After:

prototypes:
  standard:
    formElementsDefinition:
      Text:
        properties:
          containerClassAttribute: 'form-element form-element-text mb-3'
          elementClassAttribute: form-control
          elementErrorClassAttribute: ~
          labelClassAttribute: form-label
Copied!

Breaking: #106863 - TCA control option is_static removed 

See forge#106863

Description 

The TCA control option $GLOBALS['TCA'][$table]['ctrl']['is_static'] has been removed, as it is no longer evaluated by the TYPO3 Core.

Originally, this option was introduced to mark certain database tables (for example, from static_info_tables) as containing static, non-editable reference data. Over time, the TYPO3 ecosystem has evolved, and the original purpose of is_static has become obsolete.

Modern TYPO3 installations rarely rely on static data tables. Better mechanisms now exist for managing read-only or reference data, such as the TCA options readOnly and editlock, or backend access control. Removing this legacy option improves maintainability and reduces complexity for newcomers.

Impact 

The option is_static is no longer evaluated. It is automatically removed at runtime by a TCA migration, and a deprecation log entry is generated to indicate where adjustments are required.

Affected installations 

All TYPO3 installations defining $GLOBALS['TCA'][$table]['ctrl']['is_static'] in their TCA configuration are affected.

Migration 

Remove the is_static option from the ctrl section of your TCA configuration.

Breaking: #106869 - Remove static function parameter in AuthenticationService 

See forge#106869

Description 

The method \TYPO3\CMS\Core\Authentication\AuthenticationService::processLoginData() no longer accepts the parameter $passwordTransmissionStrategy. Additionally, the method now declares a strict return type.

Impact 

Authentication services extending or overriding AuthenticationService and its method processLoginData() (or a subtype such as processLoginDataBE() or processLoginDataFE()) will no longer receive the $passwordTransmissionStrategy parameter.

Affected installations 

TYPO3 installations with custom authentication services that extend AuthenticationService and implement or override processLoginData() or one of its subtypes.

Migration 

Extensions extending AuthenticationService must remove the $passwordTransmissionStrategy parameter from their method signature and add the strict return type bool|int.

Extensions implementing subtype methods such as processLoginDataBE() or processLoginDataFE() must also remove the parameter, as it is no longer passed to these methods.

Breaking: #106949 - Duplicate doktype restriction configuration removed 

See forge#106949

Description 

The TSconfig option mod.web_list.noViewWithDokTypes has been removed, as it duplicated the existing configuration TCEMAIN.preview.disableButtonForDokType.

Since forge#96861, the latter has been established as the single source of truth for disabling the View button for specific doktype values.

Impact 

The option mod.web_list.noViewWithDokTypes is no longer evaluated. Only the configuration TCEMAIN.preview.disableButtonForDokType is now respected.

Affected installations 

TYPO3 installations that still rely on mod.web_list.noViewWithDokTypes in Page TSconfig to control the visibility of the View button in backend modules are affected.

Migration 

Remove any usage of mod.web_list.noViewWithDokTypes from Page TSconfig.

Use the existing configuration TCEMAIN.preview.disableButtonForDokType instead:

EXT:site_package/Configuration/TSconfig/Page/TCEMAIN.tsconfig
TCEMAIN.preview.disableButtonForDokType = 199, 254
Copied!

This change ensures consistent behavior and avoids duplicate configuration.

Breaking: #106964 - Enable "Light/Dark Mode" context awareness for CKEditor RTE by default 

See forge#106964

Description 

With forge#105640, context awareness for the CKEditor Rich Text Editor (RTE) was introduced, allowing the editor interface to automatically adapt to the user’s system-wide light or dark mode preference.

This feature is now enabled by default. The TYPO3 Core stylesheet EXT:rte_ckeditor/Resources/Public/Css/contents.css has been updated to support light and dark mode variants automatically. Previously fixed white backgrounds now adapt dynamically based on the editor’s preferred color scheme.

Note that this change affects only the backend editor interface. The display of RTE content in the frontend remains unaffected.

Impact 

The previously fixed light mode user interface of the CKEditor RTE is now context-aware, displaying content in light or dark mode according to the editor’s system preference.

Affected installations 

TYPO3 installations that rely on a fixed light mode presentation of CKEditor RTE instances in the backend are affected.

Migration 

Installations with custom CKEditor modifications should review their contents.css file. If the TYPO3 Core default stylesheet was previously used, and a fixed light mode appearance is desired, this can be enforced in the RTE YAML configuration:

EXT:my_extension/Configuration/RTE/MyCKPreset.yaml
editor.config.contentsCss:
  - "EXT:my_extension/Resources/Public/Css/CustomContents.css"
Copied!

Breaking: #106972 - TCA control option searchFields removed 

See forge#106972

Description 

The TCA control option $GLOBALS['TCA'][$table]['ctrl']['searchFields'] has been removed.

With the introduction of the Schema API and the \TYPO3\CMS\Core\Schema\SearchableSchemaFieldsCollector component, the handling of fields included in searches has changed. By default, all fields of suitable types, such as input or text, are now automatically considered searchable.

To manually define searchable fields, use the new searchable field configuration option within a field's TCA configuration. See the full list of supported field types here. Unsupported field types (such as file, inline, etc.) are not considered searchable and do not support the searchable option.

Impact 

The searchFields option is no longer evaluated. It is automatically removed at runtime through a TCA migration, and a deprecation log entry is generated to highlight where adjustments are required.

If suitable fields are detected that were not listed in the removed searchFields option, they are automatically set to searchable => false to preserve previous behavior.

Affected installations 

All installations that define searchFields in their TCA configuration.

Migration 

Remove the searchFields option from the ctrl section of your TCA configuration.

If needed, use the searchable option in individual field definitions to control which fields are included in the search functionality.

Breaking: #106976 - Removal of TCA search field configuration options 

See forge#106976

Description 

The following TCA field-level search configuration options have been removed:

  • search.case
  • search.pidonly
  • search.andWhere

These options were originally intended to customize backend record search behavior but have proven to be of little practical value:

  • They were not used in the TYPO3 Core,
  • They were not used in common third-party extensions,
  • Their behavior was unclear and inconsistently supported,
  • They were insufficiently documented and hard to validate.

This removal is part of the ongoing effort to simplify and streamline TCA configuration and reduce unnecessary complexity for integrators.

Impact 

These options are no longer evaluated. They are automatically removed at runtime through a TCA migration, and a deprecation log entry is generated to highlight where adjustments are required.

Affected installations 

Any installation or extension that defines one or more of these options in its TCA field configuration:

Example of removed TCA options
 'my_field' => [
     'config' => [
         'type' => 'input',
-        'search' => [
-            'case' => true,
-            'pidonly' => true,
-            'andWhere' => '{#CType}=\'text\'',
-        ],
     ],
 ],
Copied!

Migration 

Remove the obsolete search options from your TCA field configurations.

Breaking: #107047 - Remove pointer field functionality of TCA flex 

See forge#107047

Description 

One of the main features of TCA is the concept of record types. This allows using a single table for different purposes and in different contexts. The most well-known examples are the "Page Types" of the pages table and the "Content Types" of the tt_content table. For every specific type, it is possible to define which fields to display and to customize their configuration.

A special case historically has been plugin registration, which for a long time used the so-called subtypes feature of TCA. This was an additional layer below record types, configured using subtype_value_field (commonly list_type), and optionally subtypes_addlist and subtypes_excludelist to add or remove fields depending on the selected subtype.

FlexForms attached to such subtypes were configured using ds_pointerField (typically pointing to list_type,CType). This came in combination with the corresponding ds configuration, which was an array with keys combining the pointer fields, for example:

'ds_pointerField' => 'list_type,CType',
'ds' => [
    'news_pi1,list' => 'FILE:EXT:news/Configuration/FlexForm.xml',
    'default' => 'FILE:...'
],
Copied!

Over recent TYPO3 versions, this approach has been deprecated in favor of using record types exclusively for plugin registration via the CType field, making configuration cleaner and easier to understand.

The special plugin content element (CType=list) and the corresponding plugin subtype field list_type have been deprecated in Deprecation: #105076 - Plugin content element and plugin sub types and removed in Breaking: #105377 - Deprecated functionality removed. See also Important: #105538 - Plugin subtypes removed: Changes to configurePlugin() and TCA handling for related information about ExtensionUtility::configurePlugin() and ExtensionManagementUtility::addTcaSelectItemGroup().

With this change, support for ds_pointerField and the multi-entry ds array format has now been removed. The ds option now points to a single FlexForm, either directly or via a FILE: reference.

FlexForms must instead be assigned via standard types configuration using columnsOverrides.

This also affects the data structure identifier, which in the commonly used tca type is the dataStructureKey. It is now set to default if the table does not support record types or no record type-specific configuration exists. Otherwise, the dataStructureKey is set to the corresponding record type value, for example textpic.

This change affects the following PSR-14 events:

  • AfterFlexFormDataStructureIdentifierInitializedEvent
  • AfterFlexFormDataStructureParsedEvent
  • BeforeFlexFormDataStructureIdentifierInitializedEvent
  • BeforeFlexFormDataStructureParsedEvent

A fallback for TYPO3 v14 resolves comma-separated dataStructureKey values (for example, list_type,CType) to CType.

To address circular dependencies during schema building, FlexFormTools now supports both TCA Schema objects and raw TCA configuration arrays as input. The following methods accept a union type array|TcaSchema for the new $schema parameter:

  • getDataStructureIdentifier()
  • parseDataStructureByIdentifier()
  • cleanFlexFormXML()

Previously, these methods relied on $GLOBALS['TCA'] internally, which caused architectural issues. They now operate directly on the provided schema.

All calls to these methods should provide the $schema parameter with either a TcaSchema instance or a raw TCA configuration array. Since data structure resolution can be customized by extensions, the parameter is not strictly mandatory, but it is strongly recommended to provide it in most cases. An InvalidTcaSchemaException will be thrown if schema resolution is required but no schema is passed.

This change also enables components like RelationMapBuilder to use FlexFormTools during schema building, even when only raw TCA is available.

For further details on the enhanced FlexFormTools functionality, see Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

The following class has been removed as it is no longer required:

  • \InvalidCombinedPointerFieldException

Impact 

FlexForm Pointer Field Removal

Any TCA definition that still uses ds_pointerField or a ds array with multiple entries (for example news_pi1,list) will no longer work and might cause rendering errors.

FlexFormTools Schema Parameter

All code calling FlexFormTools methods ( getDataStructureIdentifier(), parseDataStructureByIdentifier(), cleanFlexFormXML()) must be updated to provide the required $schema parameter.

Affected installations 

FlexForm Pointer Field Removal

All installations using ds_pointerField (as the pointer field functionality has been removed entirely) or an array-like structure for ds in their TCA field type flex configuration.

FlexFormTools Schema Parameter

All installations with custom code that directly call FlexFormTools methods without providing the $schema parameter. This includes custom extensions or TYPO3 Core patches using these methods.

A TCA migration automatically converts single-entry ds arrays. Multi-entry definitions require manual migration, as they must be aligned with the correct record type configuration, which may require additional configuration changes beforehand.

Example for single-entry migration:

Before:

Migration of single-entry ds configuration (before)
'ds' => [
    'default' => '<T3DataStructure>...',
],
Copied!

After:

Migration of single-entry ds configuration (after)
'ds' => '<T3DataStructure>...',
Copied!

Migration 

FlexForm Pointer Field Migration

Before:

'ds_pointerField' => 'list_type,CType',
'ds' => [
    'news_pi1,list' => 'FILE:EXT:news/Configuration/FlexForm.xml',
    'default' => '<T3DataStructure>...',
],
Copied!

After:

'columns' => [
    'pi_flexform' => [
        'config' => [
            'ds' => '<T3DataStructure>...',
        ],
    ],
],
'types' => [
    'news_pi1' => [
        'columnsOverrides' => [
            'pi_flexform' => [
                'config' => [
                    'ds' => 'FILE:EXT:news/Configuration/FlexForm.xml',
                ],
            ],
        ],
    ],
],
Copied!

If no columnsOverrides is defined, the default ds value of the field configuration will be used as before.

FlexFormTools Schema Parameter Migration

Before:

use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row
);
Copied!

After:

use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Option 1: Using TCA Schema object (recommended for normal usage)
$tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
$tcaSchema = $tcaSchemaFactory->get('tt_content');
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $tcaSchema
);

// Option 2: Using raw TCA array (for schema building contexts)
$rawTca = $fullTca['tt_content'];
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $rawTca
);
Copied!

Breaking: #107229 - Removed support for annotations in Extbase 

See forge#107229

Description 

Extbase no longer supports PHP annotations for models, data transfer objects (DTOs), and controller actions. Use PHP attributes (for example #[Extbase\Validate]) instead. Attributes provide the same functionality with better performance and native language support.

Extbase previously relied on annotation parsing, typically detected when the annotation namespace was imported, for example:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    /**
     * @Extbase\Validate("NotEmpty")
     */
    protected string $foo = '';
}
Copied!

Since TYPO3 now requires PHP 8.2 as the minimum version, the use of native PHP attributes is preferred. All Extbase-related annotations have been available as PHP attributes since TYPO3 v12.

Impact 

By dropping support for Extbase annotations, only PHP attributes are now supported. This provides more type safety and better integration with PHP’s language features. Developers benefit from a cleaner, faster, and more reliable implementation without the need for the deprecated third-party annotation parser.

Affected installations 

All Extbase models, Data Transfer Objects (DTOs), and controllers that use Extbase annotations are affected. This includes built-in annotations such as Cascade, Lazy, and Validate, as well as any custom annotation implementations.

Migration 

Switch from Extbase annotations to native PHP attributes.

Before (with annotations) 

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    /**
     * @Extbase\Validate("NotEmpty")
     */
    protected string $foo = '';
}
Copied!

After (with PHP attributes) 

use TYPO3\CMS\Extbase\Attribute as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

Breaking: #107310 - Remove FreezableBackendInterface 

See forge#107310

Description 

The interface \TYPO3\CMS\Core\Cache\Backend\FreezableBackendInterface has been removed from the TYPO3 Core.

It previously defined the following methods:

  • freeze() — Freezes the cache backend.
  • isFrozen() — Returns whether the backend is frozen.

Impact 

Any code implementing or referencing \FreezableBackendInterface will now trigger a PHP fatal error.

Since this interface was never implemented in the TYPO3 Core and had no known real-world usage, the overall impact is expected to be minimal.

Affected installations 

Installations with custom extensions that implement or reference the \FreezableBackendInterface are affected.

Migration 

Remove any references to \FreezableBackendInterface from your extension code.

If you require freeze functionality, implement the desired behavior directly in your custom cache backend class.

Breaking: #107323 - Workspace "Freeze Editing" removed 

See forge#107323

Description 

The workspace record flag Freeze Editing has been removed without replacement, effectively removing this feature.

The functionality had only been partially implemented and exhibited several usability and conceptual issues. It was therefore decided to remove it entirely in favor of more reliable and flexible workspace configuration options.

Impact 

"Freezing" a workspace to prevent editing of records is no longer possible.

Affected installations 

Since Freeze Editing did not provide any visible feedback to editors, it was likely used only rarely.

During the database compare, the field freeze will be removed from the table sys_workspace, effectively "unfreezing" all previously frozen workspaces.

Migration 

A more robust alternative to Freeze Editing is to configure a custom workspace stage and assign responsible persons. In combination with the following workspace options:

  • Publish only content in publish stage
  • Restrict publishing to workspace owners

you can create a workflow in which only designated members or groups are permitted to move records into the Ready to publish stage and/or perform publishing actions.

While other members can still edit records, such edits will automatically reset the workspace stage back to Editing, restarting the review cycle for those changes.

Breaking: #107324 - Streamline PSR-7 Response Header Handling 

See forge#107324

Description 

The handling of PSR-7 response headers in TYPO3 Core and Extbase has been unified. Previously, different mechanisms caused inconsistent behavior:

  • Extbase only kept the last value of a header, discarding all previous values (e.g. only one Set-Cookie header was possible).
  • Core allowed multiple Set-Cookie headers, but merged all other headers with multiple values into a single comma-separated string. According to RFC 9110, this is only valid for headers that explicitly support comma-separated lists.

With this change, TYPO3 now preserves multiple header values by default. Each value is emitted as a separate header line, while single values remain a single-line header.

Impact 

  • Multiple header values are now always emitted as multiple header lines.
  • Extbase and Core responses can now properly emit multiple headers with the same name (e.g. Set-Cookie, WWW-Authenticate, Link, xkey).
  • Extensions that relied on the old merging or overwriting behavior may need to be adapted.

Affected installations 

Installations are affected if they:

  • Relied on headers being merged into a comma-separated string.
  • Relied on only the last header value being retained in Extbase responses.

Migration 

If your use case requires merged header values, you must now implement this explicitly:

use TYPO3\CMS\Core\Http\Response;

$response = new Response();
$values = ['foo', 'bar', 'baz'];
$response = $response->withHeader('X-Foo-Bar', implode(', ', $values));
Copied!

If your use case requires that only the last header value is retained, you must also handle this explicitly in your code:

use TYPO3\CMS\Core\Http\Response;

$response = new Response();
$values = ['foo', 'bar', 'baz'];
$response = $response->withHeader('X-Foo-Bar', end($values));
Copied!

Note: There is another edge case not affected by this change. Multiple Extbase plugins still cannot set multiple header values with the same name (for example, two Extbase plugins both setting a Set-Cookie header). In this case, the latter will override the former. Installations affected by this scenario should resolve it by adding their own middleware.

Breaking: #107343 - Removed "beforeFormCreate" hook 

See forge#107343

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] has been removed in favor of the PSR-14 event BeforeFormIsCreatedEvent , which provides a more powerful and flexible way to influence form creation.

Impact 

Any hook implementation registered under $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] is no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The Extension Scanner will report such usages as a weak match.

Migration 

The hook has been removed without prior deprecation. This allows extensions to remain compatible with both TYPO3 v13 (using the hook) and v14+ (using the new event) simultaneously.

Use the PSR-14 event BeforeFormIsCreatedEvent to extend or modify form creation behavior.

Breaking: #107356 - Use Record API in List Module 

See forge#107356

Description 

The Content > List backend module has been refactored to use the \RecordInterface and related Record API internally instead of working with raw database row arrays.

This modernization introduces stricter typing and improves data consistency. As a result, several public method signatures have been updated.

The following public methods in DatabaseRecordList have changed their signatures:

  • renderListRow() now expects a RecordInterface object instead of an array as the second parameter.
  • makeControl() now expects a RecordInterface object instead of an array as the second parameter.
  • makeCheckbox() now expects a RecordInterface object instead of an array as the second parameter.
  • languageFlag() now expects a RecordInterface object instead of an array as the second parameter.
  • makeLocalizationPanel() now expects a RecordInterface object instead of an array as the second parameter.
  • linkWrapItems() now expects a RecordInterface object instead of an array as the fourth parameter.
  • getPreviewUriBuilder() now expects a RecordInterface object instead of an array as the second parameter.
  • isRecordDeletePlaceholder() now expects a RecordInterface object instead of an array.
  • isRowListingConditionFulfilled() has dropped the first parameter $table and now expects a RecordInterface object instead of an array.

These changes enable the List module to operate on structured Record objects, providing better type safety, consistency, and a foundation for further modernization of the backend record handling.

Impact 

Code that calls these methods directly must be updated to pass \RecordInterface objects instead of database row arrays.

Affected installations 

TYPO3 installations with custom extensions that:

  • Extend or XCLASS DatabaseRecordList and override any of the affected methods.
  • Call the affected methods directly with array-based record data.

Migration 

When calling affected methods, use the Record API to create a Record object from a database row:

Before:

Migrating from array-based record handling to the Record API (before)
$databaseRecordList->renderListRow($table, $rowArray, $indent, $translations, $enabled);
Copied!

After:

Migrating from array-based record handling to the Record API (after)
use TYPO3\CMS\Backend\RecordList\DatabaseRecordList;
use TYPO3\CMS\Backend\Record\RecordFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$recordFactory = GeneralUtility::makeInstance(RecordFactory::class);
$record = $recordFactory->createResolvedRecordFromDatabaseRow($table, $rowArray);
$databaseRecordList->renderListRow($table, $record, $indent, $translations, $enabled);
Copied!

Breaking: #107380 - Removed "beforeFormDuplicate" hook 

See forge#107380

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDuplicate'] has been removed in favor of the more powerful PSR-14 event BeforeFormIsDuplicatedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107382 - Removed "beforeFormDelete" hook 

See forge#107382

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] has been removed in favor of the more powerful PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107388 - Removed "beforeFormSave" hook 

See forge#107388

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] has been removed in favor of the more powerful PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107397 - Circular dependency between ProcessedFile and Task removed 

See forge#107397

Description 

The circular dependency between \TYPO3\CMS\Core\Resource\ProcessedFile and File Processing Task classes has been resolved to improve the architecture and maintainability of the File Abstraction Layer (FAL) processing system.

The following changes have been made to ProcessedFile :

  • The public method getTask() has been removed.
  • The public method generateProcessedFileNameWithoutExtension() has been removed.

The following changes have been made to ProcessedFileRepository :

  • Method add() now requires a TaskInterface parameter.
  • Method update() now requires a TaskInterface parameter.

Additionally, the checksum validation logic has been moved from ProcessedFile to AbstractTask .

Impact 

Any code that calls the following methods will cause PHP fatal errors:

  • ProcessedFile->getTask()
  • ProcessedFile->generateProcessedFileNameWithoutExtension()

Any code that calls ProcessedFileRepository->add() or ProcessedFileRepository->update() without the new TaskInterface parameter will cause PHP fatal errors.

Code that relied on TYPO3CMSCoreResourceProcessedFile objects having Task objects available internally will no longer work, as Task objects are now created externally by FileProcessingService when needed.

Affected installations 

Installations with custom file processing extensions or custom Task implementations that directly interact with the ProcessedFile->getTask() method are affected. The extension scanner will report any usage of ProcessedFile->getTask() and ProcessedFile->generateProcessedFileNameWithoutExtension() as weak matches.

Extensions that manually call ProcessedFileRepository->add() or ProcessedFileRepository->update() are also affected. The extension scanner will not report usages of these methods due to too many weak matches.

Migration 

Replace calls to ProcessedFile->getTask() with direct creation of Task objects through the TaskTypeRegistry :

Before:

$task = $processedFile->getTask();
Copied!

After:

use TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$taskTypeRegistry = GeneralUtility::makeInstance(TaskTypeRegistry::class);
$task = $taskTypeRegistry->getTaskForType(
    $processedFile->getTaskIdentifier(),
    $processedFile,
    $processedFile->getProcessingConfiguration()
);
Copied!

It is recommended to implement your own alternative to ProcessedFile->generateProcessedFileNameWithoutExtension() if similar logic is still needed.

Update calls to ProcessedFileRepository->add() and ProcessedFileRepository->update() to include the new TaskInterface parameter:

Updated calls to ProcessedFileRepository
use TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before
$processedFileRepository->add($processedFile);
$processedFileRepository->update($processedFile);

// After
$taskTypeRegistry = GeneralUtility::makeInstance(TaskTypeRegistry::class);
$task = $taskTypeRegistry->getTaskForType(
    $processedFile->getTaskIdentifier(),
    $processedFile,
    $processedFile->getProcessingConfiguration()
);

$processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
$processedFileRepository->add($processedFile, $task);
$processedFileRepository->update($processedFile, $task);
Copied!

Breaking: #107403 - Remove LocalPreviewHelper + LocalCropScaleMaskHelper 

See forge#107403

Description 

The helper classes for preview and CropScaleMask (CSM) image generation prevented further unification of the File Abstraction Layer (FAL) image processing API.

The two helper classes \TYPO3\CMS\Core\Resource\Processing\LocalPreviewHelper and \TYPO3\CMS\Core\Resource\Processing\LocalCropScaleMaskHelper have been removed. Their functionality has been merged into \TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor .

These helper classes existed for historical reasons and were never intended to be part of the public API.

Impact 

Any code that extends or references \LocalPreviewHelper or \LocalCropScaleMaskHelper will now trigger PHP fatal errors.

Affected installations 

Installations with custom extensions that extend or reference either \LocalPreviewHelper or \LocalCropScaleMaskHelper are affected and will cause PHP fatal errors.

These helper classes were meant to be internal but were never declared as such. Implementations using them directly instead of the LocalImageProcessor should be very rare.

Migration 

Remove all references to \LocalPreviewHelper or \LocalCropScaleMaskHelper from your code.

Use the LocalImageProcessor directly instead, or implement a custom image processor that executes before this processor to apply additional functionality.

Breaking: #107436 - Localization system architecture changes 

See forge#107436

Description 

The TYPO3 localization system has been migrated to use the Symfony Translation components internally (see Feature: #107436 - Symfony Translation Component integration), which introduces several breaking changes to the internal API and configuration handling.

This change affects only the internal processing of translation ("locallang") files. The public API of the localization system remains unchanged.

Method Signature Changes

The method \TYPO3\CMS\Core\Localization\LocalizationFactory::getParsedData() has a modified signature and behavior:

Before:

public function getParsedData(
    $fileReference,
    $languageKey,
    $_ = null,
    $__ = null,
    $isLocalizationOverride = false
)
Copied!

After:

public function getParsedData(string $fileReference, string $languageKey): array
Copied!

It now only returns the parsed and combined localization data instead of both the default and the localized data. To obtain the default (English) data, call getParsedData($fileReference, 'en').

Language Key Changes

Internally, the fallback language key has been changed from default to en. This does not affect public API usage, where default can still represent any configured language key other than English.

Configuration Changes

Custom parser configuration is no longer supported. The global configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] has been removed, and any custom parser registered through it will be ignored.

Several configuration options have been moved or renamed:

  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['requireApprovedLocalizations'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['format'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['format']
  • $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']

Impact 

Code calling \TYPO3\CMS\Core\Localization\LocalizationFactory::getParsedData() with the old signature or expecting a multi-dimensional array structure will break:

  • The method now enforces strict parameter types.
  • The method returns en instead of default for English translations.
  • Unused parameters have been removed.

Extensions that register custom localization parsers using $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] will no longer have their parsers executed, potentially leading to missing translations.

Instances using the moved configuration options must update their configuration to the new paths. The old options are no longer recognized and will be ignored.

Affected installations 

Installations that:

  • Call LocalizationFactory::getParsedData() directly.
  • Register custom localization parsers via the removed configuration option.
  • Use any of the renamed or moved configuration options listed above.

Migration 

Method Call Updates

Update all calls to getParsedData() to use the new signature and ensure parameter types are correct:

Before:

Updated getParsedData() usage
$data = $factory->getParsedData(
    $fileReference,
    $languageKey,
    null,
    null,
    false
)[$languageKey];
Copied!

After:

$data = $factory->getParsedData($fileReference, $languageKey);
Copied!

Custom Parser Migration

Replace any custom localization parser with a Symfony Translation loader. See Feature: #107436 - Symfony Translation Component integration for detailed migration instructions to the new loader system.

Breaking: #107438 - Default parseFunc configuration for Fluid Styled Content 

See forge#107438

Description 

Since TYPO3 v13, the basic lib.parseFunc and lib.parseFunc_RTE configurations are always available through EXT:frontend/ext_localconf.php.

The default parseFunc configuration previously provided by EXT:fluid_styled_content has been removed to avoid duplicated and outdated settings. This unifies the parseFunc behavior between EXT:frontend and EXT:fluid_styled_content.

The allowTags syntax is no longer set by default, as HTML sanitization has been handled properly by the HTML Sanitizer for some time. All HTML tags are now allowed by default in frontend output, while the HTML Sanitizer controls which tags and attributes are ultimately permitted.

Note that the allowTags directive itself has not been removed. It can still be set to restrict frontend output where desired.

The conceptual approach is:

  • The backend (RTE and its HTML parser/processing) already cleans unwanted content and controls what is stored in the database.
  • The frontend output (parseFunc) should only add additional restrictions through allowTags when content comes from external or untrusted sources (for example, custom Extbase output).

Impact 

Custom TypoScript configurations using allowTags syntax may no longer work as expected. Specifically:

  • allowTags := addToList(wbr) no longer appends wbr. Instead, it limits allowed tags to only wbr.
  • Default CSS classes on HTML elements (for example <table class="contenttable">) are no longer automatically added by the parseFunc configuration.
  • The parseFunc configuration in EXT:fluid_styled_content no longer provides custom link-handling options such as extTarget and keep.

Affected installations 

TYPO3 installations that use:

  • Custom TypoScript configurations relying on prior default allowTags behavior.
  • Extensions or sites depending on the specific parseFunc configuration previously provided by EXT:fluid_styled_content.
  • Configurations expecting automatic CSS class additions to HTML elements.
  • Sites relying on the old external link-handling behavior from the Fluid Styled Content parseFunc.

Migration 

If you need to allow specific HTML tags, explicitly configure allowTags instead of extending a former default.

Before (no longer works):

lib.parseFunc_RTE.allowTags := addToList(wbr)
Copied!

After:

lib.parseFunc_RTE.allowTags = b,span,i,em,wbr,...
Copied!

For custom CSS classes on HTML elements, use custom CSS or add them through Fluid templates or TypoScript processing instead.

If you require the previous link-handling behavior, configure it explicitly:

lib.parseFunc_RTE {
    makelinks {
        http {
            extTarget = _blank
            keep = path
        }
    }
}
Copied!

Breaking: #107443 - Migrate Modal component from Bootstrap to native dialog 

See forge#107443

Description 

The TYPO3 Modal component has been migrated from Bootstrap's modal implementation to use the native HTML <dialog> element. This improves accessibility, reduces bundle size, and removes the dependency on Bootstrap's JavaScript for modal functionality.

As part of this migration, Bootstrap's native modal events (such as show.bs.modal, shown.bs.modal, hide.bs.modal, and hidden.bs.modal) are no longer dispatched.

Likewise, direct Bootstrap modal API usage (for example new Modal() from Bootstrap or $(element).modal()) is no longer supported.

Impact 

Bootstrap modal events ( *.bs.modal) are no longer available.

Extensions listening to these events must migrate to TYPO3's custom modal events.

The Modal component now uses the native <dialog> element with updated CSS classes and DOM structure.

Any direct manipulation of Bootstrap modal APIs will no longer work.

Extensions using data-bs-toggle="modal", data-bs-content="...", or data-bs-target attributes to trigger modals must migrate to TYPO3's Modal API.

Affected installations 

All installations with custom extensions that:

  • Listen to Bootstrap modal events ( show.bs.modal, shown.bs.modal, hide.bs.modal, hidden.bs.modal)
  • Use Bootstrap's modal JavaScript API directly (for example new bootstrap.Modal())
  • Use jQuery to control modals (for example $(element).modal('show'))
  • Use data-bs-toggle="modal" or data-bs-content="..." attributes
  • Manipulate modal DOM structures or classes expecting Bootstrap markup

Migration 

Event migration 

Replace Bootstrap modal event listeners with TYPO3's custom modal events. Event listeners must be attached to the modal instance returned by the Modal API, not queried from the DOM.

Before:

const modalElement = document.querySelector('.modal');
modalElement.addEventListener('show.bs.modal', (event) => {
  console.log('Modal is about to be shown');
});
modalElement.addEventListener('shown.bs.modal', (event) => {
  console.log('Modal is now visible');
});
modalElement.addEventListener('hide.bs.modal', (event) => {
  console.log('Modal is about to be hidden');
});
modalElement.addEventListener('hidden.bs.modal', (event) => {
  console.log('Modal is now hidden');
});
Copied!

After:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

const modal = Modal.show(
  'My Modal Title',
  'This is the modal content',
  Severity.info
);

modal.addEventListener('typo3-modal-show', (event) => {
  console.log('Modal is about to be shown');
});
modal.addEventListener('typo3-modal-shown', (event) => {
  console.log('Modal is now visible');
});
modal.addEventListener('typo3-modal-hide', (event) => {
  console.log('Modal is about to be hidden');
});
modal.addEventListener('typo3-modal-hidden', (event) => {
  console.log('Modal is now hidden');
});
Copied!

Bootstrap API migration 

Replace Bootstrap modal API calls with TYPO3's Modal API.

Do not instantiate Bootstrap modals or manipulate modal DOM elements directly.

Before:

import { Modal } from 'bootstrap';

const modalElement = document.querySelector('.modal');
const bsModal = new Modal(modalElement);
bsModal.show();
bsModal.hide();
Copied!

After:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

// Show a simple modal
Modal.show(
  'My Modal Title',
  'This is the modal content',
  Severity.info,
  [
    {
      text: 'Close',
      btnClass: 'btn-default',
      trigger: (event, modal) => modal.hideModal()
    }
  ]
);

// Dismiss the current modal
Modal.dismiss();
Copied!

Data attribute migration 

Replace Bootstrap's data-bs-toggle and data-bs-target attributes with TYPO3's modal trigger API.

Before:

<button type="button"
        data-bs-toggle="modal"
        data-bs-target="#myModal"
        data-bs-content="Are you sure?">
  Open Modal
</button>
Copied!

After:

<button type="button"
        class="t3js-modal-trigger"
        data-title="Confirmation"
        data-content="Are you sure?"
        data-severity="warning"
        data-button-close-text="Cancel"
        data-button-ok-text="Confirm">
  Open Modal
</button>
Copied!

Alternatively, use the JavaScript API directly:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

document.querySelector('button').addEventListener('click', (event) => {
  Modal.confirm(
    'Confirmation',
    'Are you sure?',
    Severity.warning
  );
});
Copied!

Breaking: #107473 - TypoScript condition function getTSFE() removed 

See forge#107473

Description 

The TypoScript condition function getTSFE() has been removed.

After various properties like getTSFE().type were already removed in TYPO3 v13, the remaining parts of this functionality have now been removed in TYPO3 v14.

The most common remaining use was accessing the current page ID via getTSFE().id, which can be replaced with request.getPageArguments().getPageId().

Impact 

Conditions using getTSFE() will no longer evaluate to true and must be updated.

Affected installations 

Instances with TypoScript conditions that use the function getTSFE() are affected.

Migration 

Replace getTSFE() with an equivalent condition. For example:

Before:

[getTSFE() && getTSFE().id == 42]
Copied!

After:

[request?.getPageArguments()?.getPageId() == 42]
Copied!

Breaking: #107482 - Environment::getComposerRootPath method removed 

See forge#107482

Description 

The following method in \TYPO3\CMS\Core\Core\Environment has been removed in TYPO3 v14.0:

  • Environment::getComposerRootPath()

Since composer installers v4/v5 (which are required since TYPO3 v12), getComposerRootPath() and getProjectPath() return the same value, because the project path can no longer be changed through configuration.

Therefore, the method Environment::getComposerRootPath() has been removed. It was marked as internal from the beginning.

Impact 

Calling this method will result in a PHP error.

Affected installations 

TYPO3 installations with custom extensions or custom code that directly call the removed method are affected:

  • Environment::getComposerRootPath()

The extension scanner will report any usage as a strong match.

Migration 

Instead of calculating relative paths manually, use absolute paths or the appropriate TYPO3 APIs for path handling.

Use the following replacement:

  • Environment::getProjectPath()

Breaking: #107488 - Scheduler frequency options moved to TCA 

See forge#107488

Description 

The global configuration array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions'] , which was used to define frequency options for scheduler tasks, has been removed.

Frequency options are now configured directly in the TCA using the overrideFieldTca mechanism on the tx_scheduler_task.execution_details field.

This change improves consistency with TYPO3’s configuration patterns and provides better extensibility for scheduler task timing options.

Impact 

Extensions that previously added custom frequency options through the global frequencyOptions array will no longer see their custom options in the scheduler task frequency field.

Code that relied on reading the global frequencyOptions configuration will no longer work as expected.

Affected installations 

All installations with extensions providing custom scheduler frequency options through the global configuration array are affected.

Migration 

Extensions should migrate their frequency options from the global configuration to TCA overrides.

Before (no longer working):

// EXT:my_extension/ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions']['0 2 * * *'] =
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am';
Copied!

After (recommended approach):

// EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php

$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'][] = [
    'value' => '0 2 * * *',
    'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
];
Copied!

Migration for multiple options:

// EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php

$customFrequencyOptions = [
    [
        'value' => '0 2 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
    ],
    [
        'value' => '0 */6 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:every_6_hours',
    ],
    [
        'value' => '0 0 1 * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:monthly_first',
    ],
];

$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'] = array_merge(
    $GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'] ?? [],
    $customFrequencyOptions
);
Copied!

Breaking: #107507 - Removed EXT:form AbstractFinisher->getTypoScriptFrontendController() 

See forge#107507

Description 

The method \TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher->getTypoScriptFrontendController() has been removed.

Since the entire TypoScriptFrontendController class is being phased out, this abstract helper method has been removed as part of that cleanup.

Impact 

Calling this method in a custom EXT:form finisher will result in a fatal PHP error.

Affected installations 

TYPO3 instances using EXT:form with custom finishers that call this method are affected. The extension scanner is configured to detect such usages.

Migration 

Migration depends on what the finisher previously did with the returned class instance. The TypoScriptFrontendController properties and helper methods have been modernized, with most data now available as request attributes.

For example, accessing the cObj property can be replaced like this:

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start(
    $request->getAttribute('frontend.page.information')->getPageRecord(),
    'pages'
);
Copied!

Breaking: #107518 - Removed "initializeFormElement" hook 

See forge#107518

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent .

Impact 

Hook implementations registered under initializeFormElement are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected. The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to influence form element initialization. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107528 - Removed "beforeRemoveFromParentRenderable" hook 

See forge#107528

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent .

Impact 

Hook implementations registered under beforeRemoveFromParentRenderable are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected. The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form processing workflow. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107537 - Changes in URL generation of system resources 

See forge#107537

Description 

The following changes are considered breaking, although their impact is expected to be very low.

  • TypoScript getData function path previously returned a relative URL and now returns an absolute URL (prepended with absRefPrefix).
  • Access to FAL storages via relative path (fileadmin/templates/main.css) is limited to the default storage defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] .
  • All generated system resource URLs now include cache busting.
  • Adding custom query strings to resource identifiers no longer disables cache busting — both are now applied.

getText "path" in TypoScript 

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    data = path : EXT:core/Resources/Public/Icons/Extension.svg
}
Copied!
Result (TYPO3 classic mode – note the leading "/")
"path" result before: typo3/sysext/core/Resources/Public/Icons/Extension.svg
"path" result now: /typo3/sysext/core/Resources/Public/Icons/Extension.svg
Copied!
Result (TYPO3 Composer mode – note the leading "/")
"path" result before: _assets/5f237792cbcdc97cfceade1e16ea33d7/Icons/Extension.svg
"path" result now: /_assets/5f237792cbcdc97cfceade1e16ea33d7/Icons/Extension.svg
Copied!

Relative path to FAL Storage 

Referencing resources via relative path does only work for the default FAL storage defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . This means if you have other local FAL storages configured, you need to use the FAL resource syntax (e.g. FAL:42:/path/to/file.css) to reference files in such storage instead of using relative paths (e.g. my-other-fileadmin/path/to/file.css). It is generally recommended to use explicit resource identifiers (App resources or FAL resources), instead of relative paths.

All generated URLs now contain cache busting 

All generated resource URLs now include cache busting. For example, icon URLs that previously had no cache busting will now contain a cache-busting query string.

Additional query strings applied to the resource identifier 

When adding custom query strings to resource identifiers, TYPO3 previously disabled cache busting.

Now, both the custom query string and the cache-busting parameter are applied. If custom query strings were used as manual cache busters, you can now remove them safely.

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    data = asset : EXT:core/Resources/Public/Icons/Extension.svg?v=1234
}
Copied!
Result
result before: /typo3/sysext/core/Resources/Public/Icons/Extension.svg?v=1234
result now: /typo3/sysext/core/Resources/Public/Icons/Extension.svg?v=1234&1709051481
Copied!

Impact 

getText "path" in TypoScript 

All usages of path in TypoScript data now resolve to absolute URLs instead of relative ones.

Relative path to FAL Storage 

In installations referencing resources in additional local FAL storages using a relative path syntax, an exception is thrown.

All generated URLs now contain cache busting 

Generated URLs differ slightly from previous TYPO3 versions, especially when cache busting was not applied before.

Additional query strings applied to the resource identifier 

URLs now include both the original query string and the cache-busting parameter, resulting in different output compared to earlier TYPO3 versions.

Affected installations 

getText "path" in TypoScript 

All installations using path in TypoScript data.

Relative path to FAL Storage 

All installations referencing resources in additional local FAL storages using a relative path syntax (e.g. my-other-fileadmin/path/to/file.css).

All generated URLs now contain cache busting 

All installations having third party code, that misuses generated URLs to assume file system paths from them.

Additional query strings applied to the resource identifier 

All installations using resource identifiers with custom query strings, for example:

EXT:foo/Resources/Public/rte.css?v=123

Migration 

Relative path to FAL Storage 

Either convert the relative path to a FAL resource like so:

FAL:1:/path/to/file.css

Alternatively it is possible to convert it to a resource URI:

URI:/my-other-fileadmin/path/to/file.css

The first will add cache busting, the latter will use the URI as is.

Breaking: #107566 - Removed "afterInitializeCurrentPage" hook 

See forge#107566

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent .

Impact 

Hook implementations registered under afterInitializeCurrentPage are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form rendering process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107568 - Removed "afterSubmit" hook 

See forge#107568

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsValidatedEvent .

Impact 

Hook implementations registered under afterSubmit are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form submission process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107569 - Removed "beforeRendering" hook 

See forge#107569

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent .

Impact 

Hook implementations registered under beforeRendering are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the rendering process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107578 - Event AfterCacheableContentIsGeneratedEvent changed 

See forge#107578

Description 

The frontend rendering event \TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent has been adjusted due to the removal of the \TypoScriptFrontendController class.

The method getController() has been removed and replaced by two new methods: getContent() and setContent().

Impact 

Event listeners that call getController() will trigger a fatal PHP error and must be adapted.

Affected installations 

This event is commonly used in frontend rendering–related extensions, since it provides an opportunity to access and manipulate the fully rendered response body content at a late point in the rendering chain.

Instances with extensions listening to AfterCacheableContentIsGeneratedEvent may be affected. The extension scanner will detect such usages.

Migration 

In most cases, AfterCacheableContentIsGeneratedEvent->getController() was used within event listeners to get and modify the TypoScriptFrontendController->content property at the end of the rendering process.

The event now provides getContent() and setContent() methods to achieve the same goal more directly.

Before:

#[\TYPO3\CMS\Core\Attribute\AsEventListener('my-extension')]
public function indexPageContent(
    \TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent $event
): void {
    $tsfe = $event->getController();
    $content = $tsfe->content;
    // ... $content is manipulated here
    $tsfe->content = $content;
}
Copied!

After:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

#[AsEventListener('my-extension')]
public function indexPageContent(AfterCacheableContentIsGeneratedEvent $event): void
{
    $content = $event->getContent();
    // ... $content is manipulated here
    $event->setContent($content);
}
Copied!

Version check for TYPO3 v13/v14 compatibility:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

#[AsEventListener('my-extension')]
public function indexPageContent(
    AfterCacheableContentIsGeneratedEvent $event
): void {
    $version = new Typo3Version();
    if ($version->getMajorVersion() < 14) {
        // @todo: Remove if() when TYPO3 v13 compatibility is dropped
        $tsfe = $event->getController();
        $content = $tsfe->content;
    } else {
        $content = $event->getContent();
    }

    // ... $content is manipulated here

    if ($version->getMajorVersion() < 14) {
        // @todo: Remove if() when TYPO3 v13 compatibility is dropped
        $tsfe = $event->getController();
        $tsfe->content = $content;
    } else {
        $event->setContent($content);
    }
}
Copied!

Breaking: #107578 - Prepare EXT:adminpanel DataProviderInterface change 

See forge#107578

Description 

The typo3/cms-adminpanel system extension provides the interface \TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface . It can be used by extensions that extend the Admin Panel with custom modules and allows storing additional request-related data in the Admin Panel–specific data store.

The signature of the interface method getDataToStore() has changed.

Impact 

Extension authors may benefit from the additional argument passed with TYPO3 v14, but implementations must be adjusted accordingly.

Affected installations 

Most installations are not affected, as few extensions extend the Admin Panel. Instances with classes implementing DataProviderInterface are affected.

Migration 

Interface until TYPO3 v13:

namespace TYPO3\CMS\Adminpanel\ModuleApi;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

interface DataProviderInterface
{
    public function getDataToStore(
        ServerRequestInterface $request
    ): ModuleData;
}
Copied!

The getDataToStore() method is called by the Admin Panel after the Response has been created by the TYPO3 Core. Starting with TYPO3 v14, the method receives the ResponseInterface as an additional argument:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

public function getDataToStore(
    ServerRequestInterface $request,
    ResponseInterface $response
): ModuleData;
Copied!

Compatibility example for TYPO3 v13 and v14:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

public function getDataToStore(
    ServerRequestInterface $request,
    ?ResponseInterface $response = null
): ModuleData {
    // TYPO3 v13: $response is null
    // TYPO3 v14: $response is an instance of ResponseInterface
}
Copied!

TYPO3 v13 does not pass the second argument, so it must be nullable, and extensions should not expect to receive an instance of \ResponseInterface .

TYPO3 v14, however, provides the response instance automatically.

Breaking: #107654 - Remove random subpage option of doktype=shortcut 

See forge#107654

Description 

The random subpage option for shortcut pages has been removed from TYPO3 Core.

This option allowed shortcut pages to redirect to a random subpage, which was problematic for caching and resulted in unpredictable behavior - a "random" page was not truly random, as the page linking to this shortcut was cached.

The following changes have been made:

  • The class constant \TYPO3\CMS\Core\Domain\Repository\PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE has been removed.
  • The method signature of \TYPO3\CMS\Core\Domain\Repository\PageRepository::resolveShortcutPage() has changed from resolveShortcutPage(array $page, bool $resolveRandomSubpages = false, bool $disableGroupAccessCheck = false) to resolveShortcutPage(array $page, bool $disableGroupAccessCheck = false).
  • The TCA configuration for pages.shortcut_mode no longer includes the option Random subpage of selected/current page.

Impact 

Code using the removed constant \TYPO3\CMS\Core\Domain\Repository\PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE will fail with a PHP fatal error.

Code calling PageRepository::resolveShortcutPage() with three parameters, where the second parameter was $resolveRandomSubpages, will fail. The second parameter is now $disableGroupAccessCheck.

Shortcut pages configured to use random subpage mode will now behave as if they were configured for first subpage mode.

Affected installations 

TYPO3 installations with:

  • Extensions using the constant PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE
  • Extensions calling PageRepository::resolveShortcutPage() with the $resolveRandomSubpages parameter
  • Extensions extending PageRepository and overriding getPageShortcut()
  • Shortcut pages configured with random subpage mode in the database

Migration 

Remove any usage of PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE.

Update calls to PageRepository::resolveShortcutPage() to remove the $resolveRandomSubpages parameter:

Before (TYPO3 v13 and lower)
use TYPO3\CMS\Core\Domain\Repository\PageRepository;

$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$page = $pageRepository->resolveShortcutPage($page, false, true);
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Core\Domain\Repository\PageRepository;

$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$page = $pageRepository->resolveShortcutPage($page, true);
Copied!

For shortcut pages configured with random subpage mode, update the database records to use a different shortcut mode (for example, first subpage or a specific target page):

UPDATE pages
   SET shortcut_mode = 1
 WHERE shortcut_mode = 2;
Copied!

Breaking: #107677 - Drop prepend and append modes from TCA value picker 

See forge#107677

Description 

The prepend and append modes of the value picker specified in TCA and used in FormEngine (for example $GLOBALS['TCA']['tx_example']['columns']['example']['config']['valuePicker']['mode'] ) have been removed.

These modes were designed to insert predefined values before or after the existing input value, but they served only niche use cases and caused inconsistent behavior across different input types.

Maintaining these modes introduced unnecessary complexity in both implementation and accessibility. Prepending or appending content dynamically to user input fields could easily lead to unexpected results, break input validation, and interfere with assistive technologies such as screen readers. Additionally, this approach mixed presentation and data logic in ways that are not consistent with modern form handling patterns.

Future improvements to the value picker will focus on a consistent mode=replace behavior and may be implemented as new form input types to provide a more robust and accessible user experience.

Impact 

Any value picker TCA configuration using the mode options prepend or append will no longer have any effect. TYPO3 ignores these settings, and the picker defaults to the standard replace behavior.

Affected installations 

Installations using custom field wizard configurations or integrations that rely on the value picker with mode = prepend or mode = append are affected.

Migration 

There is no direct replacement for the removed modes.

If your implementation depends on prepending or appending content to existing values, implement a custom input type or custom form element to handle this behavior explicitly. This allows you to maintain full control over the user interface, data handling, and accessibility.

For most use cases, it is recommended to replace prepend or append logic with the standard replace value picker behavior or use dedicated UI controls that clearly indicate how values are modified.

If the value picker was used to suggest combinable values, consider listing these elements in the field description instead so that users can copy and paste them manually into the input field.

Breaking: #107712 - New method hasSubmoduleOverview() in ModuleInterface 

See forge#107712

Description 

The interface \TYPO3\CMS\Backend\Module\ModuleInterface has been extended with a new method hasSubmoduleOverview() to support the new card-based submodule overview feature introduced in Feature: #107712 - Introduce card-based submodule overview.

Impact 

All custom implementations of \TYPO3\CMS\Backend\Module\ModuleInterface must now implement the new method hasSubmoduleOverview(): bool.

Existing implementations that do not implement this method will trigger a PHP fatal error.

Affected installations 

TYPO3 installations with custom PHP code that directly implement the ModuleInterface are affected.

This is uncommon, as most backend modules use the provided \TYPO3\CMS\Backend\Module\Module class or extend from \TYPO3\CMS\Backend\Module\BaseModule .

Migration 

Add the hasSubmoduleOverview() method to your custom implementation of ModuleInterface .

The method should typically return the configured value rather than a fixed boolean:

use TYPO3\CMS\Backend\Module\ModuleInterface;

class MyCustomModule implements ModuleInterface
{
    protected array $configuration = [];

    public function hasSubmoduleOverview(): bool
    {
        // Return the configured value, defaulting to false
        return $this->configuration['showSubmoduleOverview'] ?? false;
    }
}
Copied!

This allows the behavior to be controlled through the module's configuration.

Breaking: #107777 - Use strict types in Extbase class Argument 

See forge#107777

Description 

All properties, method arguments, and return types in \TYPO3\CMS\Extbase\Mvc\Controller\Argument are now strictly typed.

Impact 

Classes extending Argument must now ensure that all overridden properties, method arguments, and return types declare strict types accordingly.

Affected installations 

TYPO3 installations with custom classes extending Argument .

Migration 

Ensure all subclasses of Argument use strict type declarations for overridden properties, method parameters, and return types.

Breaking: #107783 - Registration of metadata extractors via registerExtractionService 

See forge#107783

Description 

The method \TYPO3\CMS\Core\Resource\Index\ExtractorRegistry::registerExtractionService() has been removed in favor of automatic registration via the interface \TYPO3\CMS\Core\Resource\Index\ExtractorInterface .

Registration of metadata extractors now happens automatically when a class implements the required interface ExtractorInterface . No further registration is necessary.

Impact 

Any call to ExtractorRegistry::registerExtractionService() is now a no-op and has no effect in TYPO3 v14.0+. Metadata extractors are automatically registered via the interface.

Affected installations 

TYPO3 installations with custom extensions that register metadata extractor classes via the mentioned method in ext_localconf.php.

The extension scanner will report such usages.

Migration 

The method has been removed without deprecation in order to allow extensions to work with TYPO3 v13 (using the registration method) and v14+ (using automatic interface-based registration) without additional deprecations.

Remove the manual registration from ext_localconf.php:

EXT:my_ext/ext_localconf.php
- $extractorRegistry = GeneralUtility::makeInstance(ExtractorRegistry::class);
- $extractorRegistry->registerExtractionService(MyExtractor::class);
Copied!

Since custom extractors already implement the required interface ExtractorInterface , no further changes are required inside the extractor class itself.

Breaking: #107784 - Remove backend layout data provider registration via $GLOBALS 

See forge#107784

Description 

The possibility to register backend layout data providers via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] has been replaced by autoconfiguration using the service tag page_layout.data_provider.

The tag is automatically added when a class implements \TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface . Manual configuration via Services.yaml remains possible, especially when autoconfiguration is disabled.

Developers need to adapt existing implementations by adding the new method getIdentifier(), as outlined in Feature: #107784 - Autoconfigure backend layout data providers.

Additionally, the possibility to dynamically add backend layout data providers to the global DataProviderCollection via its add() method has been removed. Developers should register their data providers as service definitions in the container as described above.

Impact 

Using the global array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] to register backend layout data providers has no effect in TYPO3 v14.0 and later.

Affected installations 

All installations that use $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] for backend layout data provider registration are affected.

This registration is typically done in an ext_localconf.php file.

The extension scanner will report such usages.

Migration 

Migrate existing registrations to the new autoconfiguration-based approach.

Before:

EXT:my_extension/ext_localconf.php
use Vendor\MyExtension\View\BackendLayout\MyLayoutDataProvider;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['my_provider']
    = MyLayoutDataProvider::class;
Copied!

After:

EXT:my_extension/Classes/View/BackendLayout/MyLayoutDataProvider.php
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface;

final class MyLayoutDataProvider implements DataProviderInterface
{
    // ...

    public function getIdentifier(): string
    {
        return 'my_provider';
    }
}
Copied!

If you need to support multiple TYPO3 versions, you can implement both registration methods (via $GLOBALS and via autoconfiguration).

Ensure that getIdentifier() is implemented, which is backward compatible with older TYPO3 versions even if unused.

Breaking: #107789 - Core TCA and user settings showitem strings use short form references 

See forge#107789

Description 

TYPO3 Core TCA and user settings ( $GLOBALS['TYPO3_USER_SETTINGS']) configurations have been updated to use short form translation reference formats (e.g., core.form.tabs:*) instead of the full LLL:EXT: path format in showitem strings.

This change affects all core TCA showitem definitions that previously used full LLL:EXT: paths for labels. The most prominent updates are tab labels using the --div-- syntax, though this pattern may be applied to other TCA elements in the future.

Examples of changed references in tab labels:

// Before (TYPO3 v13)
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general'
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access'
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language'
Copied!
// After (TYPO3 v14)
'--div--;core.form.tabs:general'
'--div--;core.form.tabs:access'
'--div--;core.form.tabs:language'
Copied!

Impact 

Custom extensions that programmatically manipulate TCA or $GLOBALS['TYPO3_USER_SETTINGS'] showitem strings from core tables and expect the full LLL:EXT: path format will break.

This particularly affects code that:

  • Uses string search/replace operations on showitem strings to find or modify specific labels (tabs, palettes, or other elements)
  • Parses showitem strings using regular expressions expecting the LLL:EXT: pattern
  • Extracts translation keys from TCA configurations for analysis or documentation purposes
  • Builds custom TCA configurations by copying and modifying core showitem strings

Currently, the following label categories have been migrated to short-form:

Tab labels (--div--):

  • LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:*core.form.tabs:*
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.*core.form.tabs:*
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.*core.form.tabs:*

Tab labels (--div--) in TYPO3_USER_SETTINGS:

  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_datacore.form.tabs:personaldata
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:accountSecuritycore.form.tabs:account_security
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:openingcore.form.tabs:backend_appearance
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personalizationcore.form.tabs:personalization
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:resetTabcore.form.tabs:reset_configuration

Palette labels (palette definitions):

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.palettes.*core.form.palettes:*
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.palettes.*core.form.palettes:*
  • LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.*core.form.palettes:*
  • LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.*core.form.palettes:*
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:palette.*core.form.palettes:*
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.palettes.*core.form.palettes:*

Replaced hardcoded palette label:

  • Removed the hardcoded palette name in string --palette--;Capabilities;capabilities in table sys_file_storage in favor of a label attached directly to the palette using the short syntax core.form.palettes:*.

Field label overrides removed in showitem definitions:

Field labels can be overridden in showitem definitions for types or palettes, but should rather be kept in the field definition itself. The following field label overrides have been removed from showitem strings in favor of using the field's own label definition:

  • bodytext;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:bodytext_formlabelbodytext
  • bodytext;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:field.table.bodytextbodytext
  • CType;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType_formlabelCType
  • colPos;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos_formlabelcolPos
  • header;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_formlabelheader
  • header_layout;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_layout_formlabelheader_layout
  • header_link;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_link_formlabelheader_link
  • header_position;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_position_formlabelheader_position
  • subheader;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:subheader_formlabelsubheader
  • date;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:date_formlabeldate
  • file_collections;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:file_collections.ALT.uploads_formlabelfile_collections
  • filelink_size;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:filelink_size_formlabelfilelink_size
  • image_zoom;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:image_zoom_formlabelimage_zoom
  • imageborder;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imageborderimageborder
  • image*;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imageborder_formlabelfrontend.db.tt_content:imageborder
  • imagecols;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imagecols_formlabelimagecols
  • imageorient;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imageorient_formlabelimageorient
  • imageheight;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imageheightimageheight
  • imagewidth;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imagewidthimagewidth
  • frame_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:frame_class_formlabelframe_class
  • starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.starttime_formlabelstarttime
  • endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.endtime_formlabelendtime
  • fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.fe_group_formlabelfe_group
  • media;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:media.ALT.uploads_formlabelmedia
  • sectionIndex;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:sectionIndex_formlabelsectionIndex
  • linkToTop;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:linkToTop_formlabellinkToTop
  • layout;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:layout_formlabellayout
  • space_before_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:space_before_class_formlabelspace_before_class
  • space_after_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:space_after_class_formlabelspace_after_class
  • doktype;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype_formlabeldoktype
  • shortcut_mode;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.shortcut_mode_formlabelshortcut_mode
  • shortcut;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.shortcut_formlabelshortcut
  • mount_pid_ol;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.mount_pid_ol_formlabelmount_pid_ol
  • mount_pid;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.mount_pid_formlabelmount_pid
  • url;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.url_formlabelurl
  • title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabeltitle
  • nav_title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.nav_title_formlabelnav_title
  • subtitle;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.subtitle_formlabelsubtitle
  • nav_hide;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide_toggle_formlabelnav_hide
  • extendToSubpages;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.extendToSubpages_formlabelextendToSubpages
  • abstract;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.abstract_formlabelabstract
  • keywords;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.keywords_formlabelkeywords
  • author;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.author_formlabelauthor
  • author_email;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.author_email_formlabelauthor_email
  • lastUpdated;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.lastUpdated_formlabellastUpdated
  • newUntil;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.newUntil_formlabelnewUntil
  • backend_layout;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.backend_layout_formlabelbackend_layout
  • backend_layout_next_level;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.backend_layout_next_level_formlabelbackend_layout_next_level
  • module;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.module_formlabelmodule
  • content_from_pid;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.content_from_pid_formlabelcontent_from_pid
  • cache_timeout;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.cache_timeout_formlabelcache_timeout
  • l18n_cfg;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg_formlabell18n_cfg
  • is_siteroot;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.is_siteroot_formlabelis_siteroot
  • no_search;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.no_search_formlabelno_search
  • php_tree_stop;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.php_tree_stop_formlabelphp_tree_stop
  • editlock;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.editlock_formlabeleditlock
  • media;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.media_formlabelmedia
  • tsconfig_includes;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tsconfig_includestsconfig_includes
  • TSconfig;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.TSconfig_formlabelTSconfig

Field label overrides changed in palette definitions:

  • hidden;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.hidden_toggle_formlabelhidden;core.db.pages:hidden
  • hidden;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:field.default.hiddenhidden;frontend.db.tt_content:hidden
  • hidden;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.hidden_formlabelhidden;core.db.pages:hidden
  • starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabelstarttime;core.db.general:starttime
  • endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabelendtime;core.db.general:endtime
  • fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabelfe_group;core.db.general:fe_group
  • target;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.target_formlabeltarget;core.db.pages:link.target

Affected installations 

Installations with custom extensions that:

  • Programmatically read and manipulate TCA showitem strings from $GLOBALS['TCA'] or $GLOBALS['TYPO3_USER_SETTINGS'].
  • Override core TCA or TYPO3_USER_SETTINGS by copying and modifying existing showitem configurations
  • Perform string operations on showitem definitions expecting specific LLL:EXT: path formats
  • Generate documentation or analysis tools based on TCA label path references

The extension scanner will not detect these usages, as they involve runtime string manipulation rather than direct PHP API usage.

Note: Additional TCA elements beyond tab labels may follow this pattern in future TYPO3 versions, further extending the use of short-form references in showitem strings.

Migration 

TCA Migration 

Extension developers should review their Configuration/TCA/Overrides/ files and any PHP code that manipulates TCA showitem strings programmatically.

Option 1: Support both formats in string operations

Update your code to handle both the old LLL:EXT: path format and the new short-form references:

// Before - hardcoded search for old format
if (str_contains($showitem, 'LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general')) {
    // Will not work in TYPO3 v14+
}
Copied!
// After - handle new format
if (str_contains($showitem, 'core.form.tabs:general') ||
    str_contains($showitem, 'LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general')) {
    // Works in both versions
}
Copied!

Option 2: Use the TCA API instead of string manipulation

Rather than manipulating showitem strings directly, use TYPO3's TCA manipulation APIs:

// Instead of string manipulation
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

// Add fields using the API
ExtensionManagementUtility::addToAllTCAtypes(
    'tx_myext_domain_model_foo',
    'my_field',
    '',
    'after:title'
);
Copied!

Recommended action for all extension developers

Scan your extension's Configuration/TCA/Overrides/ directory and any PHP code that interacts with showitem strings for patterns such as:

  • str_contains(), str_replace(), preg_match() or similar string functions operating on showitem values
  • String operations looking for 'LLL:EXT:' patterns in TCA configurations
  • Custom parsing of $GLOBALS['TCA'] showitem strings expecting specific path formats

This review is especially important since future TYPO3 versions may further expand the use of short-form references across additional TCA elements.

TYPO3_USER_SETTINGS migrations 

Update your code to handle the new short form references:

Before:

EXT:my_extension/ext_tables.php
// Before - hardcoded search for old format
$showitem = $GLOBALS['TYPO3_USER_SETTINGS']['showitem'];
if (str_contains($showitem, 'LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_data')) {
    // Will not work in TYPO3 v14+
}
Copied!

After:

EXT:my_extension/ext_tables.php
// After - handle new format
$showitem = $GLOBALS['TYPO3_USER_SETTINGS']['showitem'];
if (str_contains($showitem, 'core.form.tabs:personaldata')
    || str_contains($showitem, 'LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_data')) {
    // Works in both versions
}
Copied!

Breaking: #107791 - Report interfaces removed 

See forge#107791

Description 

The Reports module has been refactored to use native backend submodules instead of dynamic service-based report registration. Individual reports are now registered as proper backend submodules in Configuration/Backend/Modules.php.

As a result, the following public interfaces have been removed:

  • \TYPO3\CMS\Reports\ReportInterface
  • \TYPO3\CMS\Reports\RequestAwareReportInterface

Impact 

Extensions that register custom reports by implementing \TYPO3\CMS\Reports\ReportInterface or \TYPO3\CMS\Reports\RequestAwareReportInterface will no longer work.

These reports will no longer appear in the backend Reports module.

Affected installations 

TYPO3 installations with custom extensions that provide reports by implementing \ReportInterface or \RequestAwareReportInterface.

Migration 

Custom reports must be migrated to backend submodules.

Register as a submodule under `system_reports`:

EXT:my_extension/Configuration/Backend/Modules.php
use Vendor\MyExtension\Controller\MyReportController;

return [
    'system_reports_myreport' => [
        'parent' => 'system_reports',
        'access' => 'admin',
        'path' => '/module/system/reports/myreport',
        'iconIdentifier' => 'module-reports',
        'labels' => [
            'title' => 'my_extension.messages:myreport.title',
            'description' => 'my_extension.messages:myreport.description',
        ],
        'routes' => [
            '_default' => [
                'target' => MyReportController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The controller should implement a standard PSR-7 request handler that returns a \Psr\Http\Message\ResponseInterface instance.

Alternatively, you can create a standalone module with showSubmoduleOverview enabled if you need to group multiple reports under your own container module.

Breaking: #107823 - Strict typing and API cleanup in backend template components 

See forge#107823

Description 

The backend template components system (buttons, dropdown items, and menus) has been modernized with strict type hints, consistent return types, and improved architecture to enhance type safety and developer experience.

Impact 

Extensions that implement or extend backend template components must verify their type declarations and update any usage of changed methods.

New ComponentInterface 

A new \TYPO3\CMS\Backend\Template\Components\ComponentInterface has been introduced as the parent interface for both \ButtonInterface and \DropDownItemInterface. This unifies the common contract for all renderable backend components.

Both interfaces now extend ComponentInterface , which defines:

  • isValid(): bool
  • getType(): string
  • render(): string

Custom implementations of \ButtonInterface or \DropDownItemInterface will now trigger a \TypeError if these return types are missing.

PositionInterface enforced 

The \TYPO3\CMS\Backend\Template\Components\PositionInterface now enforces strict type hints:

  • getPosition(): string
  • getGroup(): int

This interface allows buttons to define their own fixed position and group, which automatically override the position and group parameters passed to ButtonBar::addButton().

Icon nullability 

Icons are now consistently nullable across all button types. The AbstractButton::$icon property and related getter/setter methods now use ?Icon instead of Icon.

This affects classes extending AbstractButton , including LinkButton , InputButton , and SplitButton .

Method signature changes 

Several methods now have stricter parameter types or modified signatures:

  • MenuItem::isValid() and Menu::isValid() no longer accept parameters
  • AbstractDropDownItem::render() now declares a string return type
  • Various setter methods now require strict type hints for their parameters

Return type consistency 

Abstract classes now use static return types for better inheritance support, while concrete implementations may use self or static depending on extensibility requirements.

SplitButton API improvement 

The SplitButton::getButton() method has been replaced with getItems(), which returns a type-safe SplitButtonItems DTO instead of an untyped array.

Old (removed):

public function getButton(): array  // Returns array with magic keys 'primary' and 'options'
Copied!

New:

public function getItems(): SplitButtonItems  // Returns typed DTO
Copied!

The SplitButtonItems DTO provides:

  • public readonly AbstractButton $primary - The primary action button
  • public readonly array $options - Array of option buttons

This change improves type safety and prevents runtime errors from accessing non-existent array keys.

Affected installations 

All TYPO3 instances with custom backend components, such as buttons, menus, or dropdown items, that extend or implement the affected interfaces are impacted.

Migration 

Extension authors should:

  1. Verify custom button implementations have correct return types on interface methods.
  2. Check custom classes extending abstract buttons use proper strict types.
  3. Update `isValid()` calls on MenuItem and Menu objects (remove the parameter).
  4. Handle nullable icons when working with getIcon() methods.

Example: implementing ButtonInterface 

use TYPO3\CMS\Backend\Template\Components\ButtonInterface;

// Before
class CustomButton implements ButtonInterface {
    public function isValid() { ... }
    public function render() { ... }
    public function getType() { ... }
}
Copied!
use TYPO3\CMS\Backend\Template\Components\ButtonInterface;

// After
class CustomButton implements ButtonInterface {
    public function isValid(): bool { ... }
    public function render(): string { ... }
    public function getType(): string { return static::class; }
}
Copied!

Example: working with MenuItem/Menu 

// Before
if ($menuItem->isValid($menuItem)) { ... }
Copied!
// After
if ($menuItem->isValid()) { ... }
Copied!

Example: nullable icons 

// Handle nullable icon return
$icon = $button->getIcon();  // Now returns ?Icon
$html = $icon?->render() ?? '';
Copied!

Example: using SplitButton with typed DTO 

If you were directly accessing the getButton() method:

// Before
$items = $splitButton->getButton();
$primary = $items['primary'];  // Magic string key
$options = $items['options'];  // Magic string key
Copied!
// After
$items = $splitButton->getItems();
$primary = $items->primary;  // Type-safe property access
$options = $items->options;  // Type-safe property access
Copied!

Breaking: #107831 - AfterCachedPageIsPersistedEvent no longer receives TypoScriptFrontendController 

See forge#107831

Description 

The frontend rendering related event \TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent has been modified due to the removal of the class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController.

The method getController() has been removed.

Impact 

Event listeners that call getController() will now trigger a fatal PHP error and and must be adapted.

Affected installations 

Instances with extensions listening for the event AfterCachedPageIsPersistedEvent may be affected.

The extension scanner will detect and report such usages.

Migration 

In most cases, data that was previously retrieved from the \TYPO3\CMS\Frontend\Controller\`TypoScriptFrontendController instance can now be accessed through the request object, available via $event->getRequest().

See Breaking: #102621 - Most TSFE members marked internal or read-only for further details about accessing frontend-related data via the PSR-7 request.

Breaking: #107831 - Removed TypoScriptFrontendController 

See forge#107831

Description 

All remaining properties have been removed from \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController, making the class a readonly internal service used by the TYPO3 Core only.

The class will be fully removed in a later TYPO3 v14 release.

The following instance access patterns have been removed:

  • $GLOBALS['TSFE']
  • $request->getAttribute('frontend.controller')
  • $contentObjectRenderer->getTypoScriptFrontendController()

All API methods that returned an instance of \TypoScriptFrontendController - usually named getTypoScriptFrontendController() or similar - have been removed as well.

Impact 

Any remaining direct or indirect usage of \TypoScriptFrontendController will now result in a fatal PHP error.

Affected installations 

Extensions that still relied on properties or methods of \TypoScriptFrontendController are affected.

The class was already marked internal and breaking in TYPO3 v13.

In particular, extensions that used AbstractContentObject->getTypoScriptFrontendController() can now access the relevant data from the PSR-7 request object, for example:

$pageInformation = $request->getAttribute('frontend.page.information');
Copied!

Migration 

See Breaking: #102621 - Most TSFE members marked internal or read-only for a detailed list of removed properties and their replacements.

As a specific example, old code that added additional header data like this:

$frontendController = $request->getAttribute('frontend.controller');
$frontendController->additionalHeaderData[] = $myAdditionalHeaderData;
Copied!

should now use:

use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->addHeaderData($myAdditionalHeaderData);
Copied!

The same approach applies to additionalFooterData.

Breaking: #107856 - DataHandler: Remove internal property copyWhichTablesand properties neverHideAtCopyand copyTree 

See forge#107856

Description 

The following public properties of the PHP class TYPO3\CMS\Core\DataHandling\DataHandler have been removed:

  • copyWhichTables
  • neverHideAtCopy
  • copyTree

Impact 

Accessing or setting the properties will throw a PHP warning and have no effect anymore.

Affected installations 

Any installation working with the public properties in a third-party extension.

Migration 

The configuration values neverHideAtCopy and copyTree are directly read from the backend user BE_USER object. To modify them, use the following values instead:

// Before
DataHandler->neverHideAtCopy
// After
DataHandler->BE_USER->uc['neverHideAtCopy']

// Before
DataHandler->copyTree
// After
DataHandler->BE_USER->uc['copyLevels']
Copied!

To retain database consistency, the list of tables to be copied now only relied on permissions of the given backend user. If the user has admin access, all tables will be copied if needed. If not, all tables with access will be copied.

Breaking: #107871 - Remove backend avatar provider registration via $GLOBALS 

See forge#107871

Description 

Registering backend avatar providers via $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] has been replaced by autoconfiguration using the backend.avatar_provider service tag. This tag is added automatically when the PHP attribute #[AsAvatarProvider] is applied. Manual configuration in Services.yaml is still possible, particularly if autoconfiguration is disabled.

Impact 

Using the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] array has no effect in TYPO3 v14.0 and later.

Affected installations 

All installations that use $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] to register backend avatar providers are affected. This registration is typically performed in an ext_localconf.php file. The extension scanner will report any such usages.

Migration 

Migrate existing registrations to the new autoconfiguration-based approach.

Before:

EXT:my_extension/ext_localconf.php
use MyVendor\MyExtension\Backend\Avatar\MyAvatarProvider;

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']
    ['avatarProviders']['my_provider'] = [
        'provider' => MyAvatarProvider::class,
        'before' => ['provider-1'],
        'after' => ['provider-2'],
    ];
Copied!

After:

EXT:my_extension/Classes/Backend/Avatar/MyAvatarProvider.php
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
use TYPO3\CMS\Backend\Backend\Avatar\AvatarProviderInterface;

#[AsAvatarProvider(
    'my_provider',
    before: ['provider-1'],
    after: ['provider-2']
)]
final class MyAvatarProvider implements AvatarProviderInterface
{
    // ...
}
Copied!

If you need to support multiple TYPO3 Core versions simultaneously, ensure that both registration methods are implemented: the legacy $GLOBALS based registration as well as the new tag-based approach.

Breaking: #107884 - Rework actions to use Buttons API with Components 

See forge#107884

Description 

The record list and file list action system (the "button bar" in every row of the table-like display) has been completely reworked to use the Buttons API, utilizing proper component objects instead of plain HTML strings.

This modernization improves type safety, provides better extensibility, and enables more structured manipulation of action buttons through PSR-14 events.

The following components have been affected by this change:

  • \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent
  • \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent
  • \TYPO3\CMS\Backend\RecordList\DatabaseRecordList::makeControl()

Buttons can now be placed into ActionGroups, which are identified by the PHP enum \TYPO3\CMS\Backend\Template\Components\ActionGroup and distinguish between a "primary" and a "secondary" group.

In addition, \TYPO3\CMS\Backend\Template\Components\ComponentGroup enhances the ability to group multiple Button API Components into a single data object and manage their state.

Impact 

Extensions that listen to the ModifyRecordListRecordActionsEvent or ProcessFileListActionsEvent to modify record or file actions need to be updated.

The events no longer work with HTML strings but with ComponentInterface objects (see forge#107823).

Extensions that directly call DatabaseRecordList::makeControl() must update their code, as the $table parameter has been removed.

ModifyRecordListRecordActionsEvent 

The method setAction() now requires a ComponentInterface object, and getAction() now returns either null or a ComponentInterface instance.

The following methods now expect an ActionGroup enum value as the $group parameter:

  • hasAction()
  • getAction()
  • removeAction()
  • getActionGroup()

The method getRecord() no longer returns a raw array but an instance of the Record API.

A new method getRequest() has been added to access the current PSR-7 request context.

Removed methods:

  • getActions()
  • setActions()
  • getTable()

ProcessFileListActionsEvent 

The \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent has received identical API changes to ModifyRecordListRecordActionsEvent , allowing manipulation of items in both supported ActionGroup contexts (primary and secondary).

New methods:

  • setAction()
  • getAction()
  • removeAction()
  • moveActionTo()
  • getActionGroup()
  • getRequest()

Buttons can now also be repositioned or inserted at specific before/after locations within an action group.

Removed methods:

  • getActionItems()
  • setActionItems()

Affected installations 

TYPO3 installations with custom PHP code that modifies record or file list actions, or utilizes the mentioned PSR-14 events, are affected.

Migration 

DatabaseRecordList::makeControl() 

// Before
public function makeControl($table, RecordInterface $record): string

// After
public function makeControl(RecordInterface $record): string
Copied!

The $table parameter has been removed, as the table name can now be retrieved from the RecordInterface via $record->getMainType().

Adjust calls accordingly:

EXT:my_extension/Classes/ViewHelper/MyControlViewHelper.php
 // ...
 public function render(): string
 {
     $row = BackendUtility::getRecord($table, $someRowUid);
     $databaseRecordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
-    return $databaseRecordList->makeControl($table, $row);
+    return $databaseRecordList->makeControl($row);
 }
Copied!

ProcessFileListActionsEvent 

Event listeners must now compose buttons via the Button API and add each component using the event’s setAction() method.

Internally, buttons are placed into an ActionGroup container, retrieved via ActionGroup::primary or ActionGroup::secondary.

The previous getActionItems() logic is replaced with getActionGroup() to fetch the corresponding button group.

Instead of manipulating raw HTML, you must now create components using the ComponentFactory .

// Before
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

class ProcessFileListActionsEventListener
{
    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        $items = $event->getActionItems();
        $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
        unset($items['some-other-action']);
        $event->setActionItems($items);
    }
}
Copied!
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

class ProcessFileListActionsEventListener
{
    public function __construct(
        private readonly ComponentFactory $componentFactory,
        private readonly IconFactory $iconFactory,
    ) {}

    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        $viewButton = $this->componentFactory->createGenericButton()
            ->setIcon($this->iconFactory->getIcon('actions-view'))
            ->setTitle('My title');

        $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
        $event->removeAction('some-other-action', ActionGroup::primary);
    }
}
Copied!

ModifyRecordListRecordActionsEvent 

This event now behaves identically to the file list event: actions must be created via the Button API and added as ComponentInterface instances using setAction().

The setActions() and getActions() methods are removed and must be replaced by distinct calls to setAction() or use getActionGroup() to access existing actions.

The getRecord() method now returns a Record API object instead of an array. getTable() can be replaced with getRecord()->getMainType().

Modifying actions example:

// Before
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;

class ModifyRecordListRecordActionsEventListener
{
    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        $items = $event->getActions();
        unset($items['my-own-action']);
        $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
        unset($existing['some-other-action']);
        $event->setActions($items);

        $event->setAction('<button ...></button>', 'my-other-own-action', 'secondary');
    }
}
Copied!
// After
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Core\Imaging\IconFactory;

class ModifyRecordListRecordActionsEventListener
{
    public function __construct(
        private readonly ComponentFactory $componentFactory,
        private readonly IconFactory $iconFactory,
    ) {}

    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        $viewButton = $this->componentFactory->createGenericButton()
            ->setIcon($this->iconFactory->getIcon('actions-view'))
            ->setTitle('My title');

        $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
        $event->removeAction('some-other-action', ActionGroup::primary);

        $inputButton = $this->componentFactory->createInputButton()
            ->setTitle('My Button');

        $event->setAction($inputButton, 'my-other-own-action', ActionGroup::secondary);
    }
}
Copied!

Accessing groups 

// Before
$event->getAction('my-button', 'primary');
$event->hasAction('my-button', 'primary');
$event->removeAction('my-button', 'primary');
$event->getActionGroup('primary');
Copied!
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;

$event->getAction('my-button', ActionGroup::primary);
$event->hasAction('my-button', ActionGroup::primary);
$event->removeAction('my-button', ActionGroup::primary);
$event->getActionGroup(ActionGroup::primary);
Copied!

Accessing record 

// Before
$uid = $event->getRecord()['uid'];
$title = $event->getRecord()['title'];
Copied!
// After
$uid = $event->getRecord()->getUid();
$title = $event->getRecord()->getRawRecord()['title'];
Copied!

Dual-version compatibility 

To support both TYPO3 v13 and v14, extensions can use a version check within event listeners:

use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Core\Information\Typo3Version;

class ModifyRecordListRecordActionsEventListener
{
    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        if ((new Typo3Version())->getMajorVersion() >= 14) {
            $viewButton = $this->componentFactory->createGenericButton()
                ->setIcon($this->iconFactory->getIcon('actions-view'))
                ->setTitle('My title');
            $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
            $event->removeAction('some-other-action', ActionGroup::primary);
        } else {
            $items = $event->getActions();
            unset($items['my-own-action']);
            $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
            unset($existing['some-other-action']);
            $event->setActions($items);
        }
    }
}
Copied!

Breaking: #107927 - Remove "external" property / option from TypoScript and AssetRenderer 

See forge#107927

Description 

The resource property external in the PAGE properties includeCSS, includeCSSLibs, includeJS, includeJSFooter, includeJSFooterlibs and includeJSLibs is now obsolete.

This also obsoletes the External option in AssetRenderer.

Both are removed in favor of the new unified URI resource definition.

Instead of marking URIs as URIs with an additional option, prefix the URI, that shall be used with URI:, or simply use absolute URLs starting with http(s)://, where the prefix is not required.

This URI resource definition will work across the system, not only in TypoScript or AssetCollector/ AssetRenderer.

Impact 

Using the external property in TypoScript or the external option in AssetCollector will have no effect any more. If absolute URLs have been used as resources, everything will work as before. Relative URIs must be prefixed with URI: from now on, otherwise an exception is thrown.

Additionally, the string after the URI: keyword must be a valid URI, otherwise an exception is thrown as well. Before this change, invalid URIs (marked as external) would have been rendered to HTML, without any obvious feedback for developers or integrators. Browsers then ignored such invalid references.

Affected installations 

TYPO3 installations using the external property in TypoScript or the external option in AssetCollector.

Migration 

TypoScript before:

page = PAGE
page.includeCSS {
    main = https://example.com/styles/main.css
    main.external = 1
    other = /styles/main.css
    other.external = 1
}
Copied!

TypoScript after:

page = PAGE
page.includeCSS {
    main = https://example.com/styles/main.css
    other = URI:/styles/main.css
}
Copied!

PHP Code before:

$assetCollector->addStyleSheet(
    'myCssFile',
    '/styles/main.css',
    [],
    ['external' => true]
);
Copied!

PHP Code after:

$assetCollector->addStyleSheet(
    'myCssFile',
    'URI:/styles/main.css',
);
Copied!

Breaking: #107943 - Frontend and backend HTTP response compression removed 

See forge#107943

Description 

The TYPO3 frontend and backend applications previously allowed compressing their HTTP responses using the configuration options $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] and $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] .

This feature, which was always disabled by default, has now been removed.

TYPO3 will no longer compress its HTTP responses itself.

Response compression should be handled by the web server rather than the application layer.

Removing this feature avoids potential conflicts when both TYPO3 and the web server attempt to compress responses and allows modern web servers to use advanced compression algorithms such as brotli or zStandard when supported by the client.

Impact 

TYPO3 can no longer compress its HTTP responses.

This responsibility is now fully delegated to the web server.

HTTP response compression had to be explicitly enabled before, so most installations will not notice a change unless they relied on this setting.

Affected installations 

Instances that configured $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] or $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] to non-zero values are affected.

Administrators should verify that the web server applies HTTP compression by checking for a response header such as:

Content-Encoding: gzip

when requesting frontend or backend documents with a header like:

Accept-Encoding: gzip, deflate

All commonly used web servers enable this feature by default.

Migration 

The configuration toggles for the backend $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] and the frontend $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] are obsolete, existing settings in settings.php configuration files are actively removed when first using the install tool after upgrade to TYPO3 v14.

Breaking: #107944 - Frontend CSS file processing no longer removes comments and whitespaces 

See forge#107944

Description 

When the TYPO3 frontend was configured to compress included CSS assets, it also attempted to minify CSS by removing comments and certain whitespace characters.

This behavior has now been removed. The previous implementation was brittle, especially with modern CSS syntax, and provided no measurable performance benefit in either file transfer or client-side parsing.

Impact 

CSS asset files included in frontend pages may become slightly larger if they contain many comments. TYPO3’s internal CSS parsing was disabled by default and only active when explicitly enabled using $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] along with additional TypoScript configuration.

In most cases, this change has minimal or no practical impact.

Affected installations 

Instances that actively used TYPO3's built-in CSS parsing feature for frontend asset management are affected.

Migration 

If minimizing CSS file size is important, consider one of the following options:

  • Optimize or minify CSS files manually during deployment.
  • Accept that comments and whitespace are retained (usually negligible impact).
  • Preferably, integrate a dedicated frontend build chain to handle CSS and JavaScript minification.

Modern frontend build tools provide many additional advantages, such as linting, syntax validation, and advanced optimizations, which are beyond the scope of the TYPO3 Core.

Breaking: #107945 - Class FlexFormService merged into FlexFormTools 

See forge#107945

Description 

The class \TYPO3\CMS\Core\Service\FlexFormService has been merged into \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools .

The following methods are affected:

  • FlexFormService->convertFlexFormContentToArray($flexFormContent, $languagePointer = 'lDEF', $valuePointer = 'vDEF'): array is now FlexFormTools->convertFlexFormContentToArray(string $flexFormContent): array. The method name is unchanged, but the method signature has been simplified.
  • FlexFormService->convertFlexFormContentToSheetsArray(string $flexFormContent, string $languagePointer = 'lDEF', string $valuePointer = 'vDEF'): array is now FlexFormTools->convertFlexFormContentToSheetsArray(string $flexFormContent): array. Again, the name is identical, but the parameters have been reduced.
  • The helper method FlexFormService->walkFlexFormNode() has been made a private method within FlexFormTools.

Impact 

Instantiating or injecting FlexFormService remains possible in TYPO3 v14 due to a maintained class alias for backward compatibility.

This alias will be removed in TYPO3 v15.

Affected installations 

Any extensions or TYPO3 installations using FlexFormService are affected.

The extension scanner will automatically detect these usages.

Migration 

Extensions typically did not use the now internal helper method walkFlexFormNode().

In the unlikely case this private method was used, its functionality must now be implemented within the consuming extension.

The methods convertFlexFormContentToArray() and convertFlexFormContentToSheetsArray() have lost their second and third arguments.

These parameters ( lDEF and vDEF) were already fixed internally in TYPO3 and could no longer be changed, so their removal has no functional impact.

To continue using these methods, extensions should inject FlexFormTools instead of FlexFormService.

For extensions that need to remain compatible with both TYPO3 v13 and v14, it is still possible to use FlexFormService for now.

However, when adding compatibility for TYPO3 v15 (and dropping TYPO3 v13), extensions must switch fully to FlexFormTools .

Breaking: #108054 - Enforce explicit opt-in for TypoScript/TSconfig callables 

See forge#108054

Description 

To strengthen TYPO3's security posture and implement defense-in-depth principles, a new hardening mechanism has been introduced that requires explicit opt-in for methods and functions that can be invoked through TypoScript configuration.

The new PHP attribute #[\TYPO3\CMS\Core\Attribute\AsAllowedCallable] must be applied to any method that should be callable via:

  • TypoScript userFunc processing (including the USER and USER_INT content objects)
  • TypoScript stdWrap functions preUserFuncInt, postUserFunc and postUserFuncInt
  • TypoScript constant comment user functions
  • TSconfig renderFunc in suggest wizard configuration

This security enhancement implements strong defaults through explicit configuration, following the principle of least privilege.

Implementation details:

  • New \TYPO3\CMS\Core\Attribute\AsAllowedCallable PHP attribute
  • New \TYPO3\CMS\Core\Security\AllowedCallableAssertion service for validation
  • Enhanced \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction()
  • Enhanced \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::callUserFunction()

Impact 

Extension code that provides custom processing methods callable from TypoScript or TSconfig will fail with a \TYPO3\CMS\Core\Security\AllowedCallableException if the target method is not explicitly marked with the #[AsAllowedCallable] attribute.

The error message will be:

Attribute TYPO3\CMS\Core\Attribute\AsAllowedCallable required for
callback reference: ["VendorName\\ExtensionName\\ClassName","methodName"]
Copied!

Affected installations 

Scenarios using:

  • custom processing via TypoScript userFunc
  • custom processing via TypoScript constant comments
  • custom suggest wizard rendering via TSconfig renderFunc

Migration 

Add the #[AsAllowedCallable] attribute to all methods that should be callable from TypoScript or TSconfig.

TypoScript userFunc example:

EXT:my_extension/Classes/UserFunc/CustomProcessor.php
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;

class CustomProcessor
{
    #[AsAllowedCallable]
    public function process(
        string $content,
        array $conf
    ): string {
        return $content;
    }
}
Copied!

The attribute may be applied to:

  • public instance methods
  • public static methods
  • public __invoke() methods
  • custom functions in the global namespace

Native PHP functions in the global namespace must be wrapped explicitly.

Example for custom functions in the global namespace:

namespace {
    use TYPO3\CMS\Core\Attribute\AsAllowedCallable;

    #[AsAllowedCallable]
    function customGlobalUserFunction(): string
    {
        return '...';
    }

    #[AsAllowedCallable]
    function nativePhpHashWrapper(
        string $algo,
        string $data,
        bool $binary = false
    ): string {
        return \hash($algo, $data, $binary);
    }
}
Copied!

Breaking: #108055 - Removed frontend asset concatenation and compression 

See forge#108055

Description 

Introduction 

The implementation of CSS and JavaScript asset concatenation and pre-compression has been removed from the TYPO3 Core in v14.

The following TypoScript options are now obsolete:

  • Config property config.compressCss
  • Config property config.compressJs
  • Config property config.concatenateCss
  • Config property config.concatenateJs
  • The resource properties disableCompression and excludeFromConcatenation in the PAGE properties includeCSS, includeCSSLibs, includeJS, includeJSFooter, includeJSFooterlibs and includeJSLibs.

    Example:

    page = PAGE
    page.includeCSS {
        main = EXT:site_package/Resources/Public/Css/main.css
        # obsolete
        main.disableCompression = 1
        # obsolete
        main.excludeFromConcatenation = 1
    }
    Copied!

The configuration option $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] is obsolete in combination with Breaking: #107943 - Frontend and backend HTTP response compression removed. Existing settings in settings.php configuration files are automatically removed when the install tool is first used after upgrading to TYPO3 v14.

The PHP class \TYPO3\CMS\Core\Resource\ResourceCompressor has been removed.

Feature rundown 

In TYPO3 versions prior to v14, the system included a built-in mechanism to compile multiple registered CSS and JavaScript files into single files and to prepare compressed versions of those concatenated files for direct delivery by the web server.

This functionality had to be explicitly enabled by setting the global configuration option $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] and the TypoScript options config.concatenate* and optionally config.compress*. CSS and JavaScript files registered in the frontend using the PAGE.include* options were then concatenated and optionally compressed into a single file.

The implementation also concatenated external asset resources referenced via http and https, which were fetched server-side using GeneralUtility::getUrl() and merged with local resources.

Concatenation was designed to be as transparent as possible: hashes were created from all referenced file names and their contents (including external content) for each TYPO3 frontend page request, generating unique file names that changed whenever an included file or its content was modified. The resulting concatenated files were then referenced in the generated HTML page instead of the original resource links.

Pre-compression operated on top of concatenation: if enabled, a gzip- compressed .gz file was created and referenced in the HTML page response.

Configuration was handled via TypoScript, while asset registration could be performed using TypoScript or PHP with the PageRenderer. The Fluid asset ViewHelpers f:asset.css and f:asset.script register assets using the AssetCollector, introduced in TYPO3 v10. This implementation never supported concatenation or compression, as it was developed independently from the PageRenderer's asset handling and did not use the ResourceCompressor.

Concatenation and compression removal reasoning 

A closer look at the concatenation and compression functionality reveals several reasons for its removal:

  • HTTP/2 and HTTP/3: Modern HTTP versions allow multiple resource requests in parallel ("multiplexing"), making server-side asset concatenation obsolete. These versions also provide significant performance improvements on both the server and client sides compared to HTTP/1.1 with concatenation. HTTP/2 and HTTP/3 are only available via SSL (HTTPS), which is now the standard for all serious websites. All major web servers and browsers have supported at least HTTP/2 for years.
  • Fragile implementation: Concatenating multiple CSS files within the application caused path and encoding issues. The resulting files had to be stored in writable public directories, and relative paths in CSS files (such as those in @import rules) had to be parsed and adjusted. Additionally, the CSS statement @charset had to be parsed since it must appear only once per CSS file, leading to potential collisions that have never been resolved.
  • Parallel systems: Concatenation and compression were supported only for assets registered via TypoScript page.include* or the PageRenderer in PHP. The Fluid ViewHelpers f:asset.css and f:asset.script operated independently and never supported these features. Removing concatenation and compression simplifies future unification of both asset systems.
  • Performance issues with external assets: To create as few asset resources as possible, external assets (example: https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js) were by default (if excludeFromConcatenation = 1 was not explicitly set) fetched by TYPO3 when creating the page. The hashed resource path and the file content were part of the created concatenation filename. This ensured that the server-side created file was always current. TYPO3 therefore fetched external resources for each uncached page request. Worse, it fetched those external resources for each request that contained a non-cached content element ("INT" elements), and also for each request when an instance enabled CSP handling. This could easily lead to severe performance degradation.
  • Compression dependency: Pre-compression (gzip) was only available when concatenation was enabled, adding further complexity and limitations.
  • HTTP violations with caching: When TYPO3 cached a page that referenced pre-compressed assets, it stored the version based on the client’s Accept-Encoding header. Subsequent requests from clients not supporting gzip would still receive references to compressed assets, violating HTTP standards.
  • Double compression risk: Web servers might automatically re- compress already compressed files. TYPO3 tried to prevent this with an Apache-specific .htaccess configuration, but other servers like nginx required custom setups, often confusing integrators.
  • Modern compression standards: Modern web servers and browsers support more efficient algorithms such as Brotli and Zstandard, which TYPO3 never implemented due to the complexity of its existing system.
  • Minimal performance gain: The benefit of pre-compression was minor. Modern web servers can compress small assets (like typical CSS or JS files) on the fly with negligible overhead. As a rough ballpark estimation, a single CPU core can compress hundreds of MB per second, while a large JavaScript library like jQuery is usually below 100 KB. CPU time is rarely the bottleneck.

In summary, asset concatenation and pre-compression are no longer needed and should not be handled at the application level. The implementation was riddled with issues over the years and integrators struggled to reach working solutions. The Core team closed issues in this area for years with "Won't fix, use a different solution."

Alternatives 

Most TYPO3 instances can operate perfectly well without the removed asset concatenation and compression features. Modern browsers, web servers, and HTTP protocol versions provide efficient alternatives that make the previous TYPO3-internal implementation unnecessary.

If your instance still relies on concatenated or pre-built asset bundles for specific use cases, consider the following alternatives:

  • Use a modern bundler: Tools such as Vite or Webpack can handle asset concatenation, minification, and optimization during the build process. A TYPO3-specific integration is available as an extension: Vite AssetCollector.
  • Consider the `sgalinski/scriptmerger` extension: This community extension provides an alternative approach to script and stylesheet merging and compression. It may be useful for projects that cannot yet switch to build-time bundling.
  • Enable HTTP/2 or HTTP/3: Modern HTTP versions support multiplexing, allowing browsers to download multiple assets simultaneously from a single connection, eliminating the need for server-side concatenation.

    Example Apache configuration:

    <IfModule http2_module>
        Protocols h2 http/1.1
    </IfModule>
    
    # Optional: enable pre-compressed asset delivery
    AddEncoding gzip .gz
    AddType "text/javascript" .js.gz
    AddType "text/css" .css.gz
    <FilesMatch "\.(js|css)\.gz$">
        ForceType text/plain
        Header set Content-Encoding gzip
    </FilesMatch>
    Copied!

    Example nginx configuration:

    # Enable HTTP/2 on your SSL virtual host
    server {
        listen 443 ssl http2;
        server_name example.com;
    
        ssl_certificate /etc/ssl/certs/example.crt;
        ssl_certificate_key /etc/ssl/private/example.key;
    
        # Serve pre-compressed assets if available
        gzip_static on;
        # Optionally also enable on-the-fly compression
        gzip on;
        gzip_types text/css application/javascript;
    
        [...]
    }
    Copied!

    Both configurations ensure that compressed versions of static assets (e.g., .js.gz or .css.gz) are automatically delivered to clients that support gzip encoding.

    Both Apache and nginx can also cache the compressed output in memory or on disk to avoid runtime overhead. The keywords to look for are mod_deflate with mod_cache for Apache, and proxy_cache for nginx.

  • Use a Content Delivery Network (CDN): For TYPO3 instances experiencing heavy frontend traffic or high asset load, a CDN can serve static resources such as CSS, JavaScript, and images directly from distributed edge servers. This offloads delivery from the main web server, reduces latency, and improves caching efficiency.

In summary, most TYPO3 setups can safely rely on HTTP/2, modern build pipelines, and proper web server or CDN configuration to achieve optimal frontend performance without any TYPO3-internal concatenation or compression.

Impact 

The configuration toggles mentioned above are now obsolete, and TYPO3 will no longer concatenate or compress included assets.

Affected installations 

Instances that configured $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] to non-zero values and enabled the TypoScript settings described above for asset concatenation or compression are affected.

Migration 

Consider one or more of the alternatives outlined above.

Breaking: #108084 - Allow rootless paths in URI implementation 

See forge#108084

Description 

Previously, the TYPO3 implementation of \UriInterface always prefixed rootless paths (paths without a preceding slash) with a slash. With this normalization in place, it was impossible to represent rootless paths. This has now changed so that a slash is only prepended to the path when an authority (host name) is present.

Example 

Input: rootless/path/

Examples with different URIs
use TYPO3\CMS\Core\Http\Uri;

$uri = new Uri('rootless/path/');
$uriAsString = (string)$uri;
// before: /rootless/path/
// after: rootless/path/

// Same behavior with authority
$uri = (new Uri('https://example.com'))->withPath('rootless/path/');
$uriAsString = (string)$uri;
// before: https://example.com/rootless/path/
// after: https://example.com/rootless/path/

// Colon in first path segment
$uri = new Uri('rootless:path/to/resource');
$uriAsString = (string)$uri;
// before: /rootless:path/to/resource/
// after: ./rootless:path/to/resource/
Copied!

Impact 

Regarding top level TYPO3 API and functionality, nothing has changed. Required TYPO3 code has been adapted.

Third party code that uses the Uri class directly will get different results when representing rootless paths without authority. Code that relied on the normalization done by TYPO3 before is likely to break.

Since TYPO3 is always dealing with absolute paths, due to URL rewriting in the backend and the frontend, it is unlikely that much third party code relies on relative paths, so the impact is expected to be low.

Affected installations 

Third party code that is using the Uri class directly and that is representing rootless paths without authority.

Breaking: #108093 - Add respectSubmittedDataValue argument in password ViewHelper 

See forge#108093

Description 

The <f:form.password> ViewHelper now provides the argument respectSubmittedDataValue, which allows configuration of whether a submitted field value will be put into the HTML response of the form on validation errors after submission. The default value of the new argument is set to false, resulting in a submitted field value being cleared on validation errors of the form.

Impact 

A submitted password will not remain as the value for the password field if form validation fails.

Affected installations 

TYPO3 instances using the <f:form.password> ViewHelper.

Migration 

If the submitted field value of the f:form.password ViewHelper must remain on validation errors of the form, users must adapt the password ViewHelper usage as shown below:

<f:form.password name="myPassword" respectSubmittedDataValue="1" />
Copied!

Breaking: #108097 - MailMessage->send() removed 

See forge#108097

Description 

Class \TYPO3\CMS\Core\Mail\MailMessage is a data object that should not contain service methods like send(). The following methods have been removed from this class:

  • send()
  • isSent()

Impact 

Using the removed methods on instances of this class will raise fatal PHP errors.

Affected installations 

Instances that create MailMessage objects and call send() or isSent() are affected. The extension scanner is not configured to find affected code since the method names are too generic.

Migration 

The service (usually a controller class) that sends emails should be reconfigured to get an instance of MailerInterface injected and should use that service to call send().

Example before:

use TYPO3\CMS\Core\Mail\MailMessage;

final readonly class MyController
{
    public function sendMail()
    {
        $email = new MailMessage();
        $email->subject('Some subject');
        $email->send();
    }
}
Copied!

Example after:

use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Core\Mail\MailerInterface;

final readonly class MyController
{
    public function __construct(
        private MailerInterface $mailer
    ) {}

    public function sendMail()
    {
        $email = new MailMessage();
        $email->subject('Some subject');
        $this->mailer->send($email);
    }
}
Copied!

Breaking: #108113 - Globals _GET and _POST not reset to current Request data anymore 

See forge#108113

Description 

The frontend and backend application chain roughly splits like this:

  1. Bootstrap
  2. Create Request object from globals
  3. Start application
  4. Run middleware chain
  5. Run RequestHandler to create a Response by calling controllers (backend) or ContentObjectRenderer (frontend)

There was old compatibility code in RequestHandler that reset the PHP global variables _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS to values that may have been written to their Request object counterparts by middlewares.

This backwards compatibility layer has been removed.

Additionally, in frontend rendering, the global variable $GLOBALS['TYPO3_REQUEST'] is no longer populated within the PrepareTypoScriptFrontendRendering middleware. It is now set later in RequestHandler . $GLOBALS['TYPO3_REQUEST'] itself is another compatibility layer that the TYPO3 Core aims to phase out over time.

Impact 

The impact is twofold:

  • Some TYPO3 Core middlewares manipulate the Request object's "GET" parameter list ( $request->getQueryParams()) to, for example, resolve the frontend slug into the page uid. This is itself a backwards compatibility layer. Frontend-related code can no longer expect these manipulated variables to exist in the globals _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS.
  • Middlewares that are executed after PrepareTypoScriptFrontendRendering (middleware key typo3/cms-frontend/prepare-tsfe-rendering) can no longer rely on $GLOBALS['TYPO3_REQUEST'] being set.

Affected installations 

Instances running code that relies on the removed compatibility layers may fail or lead to unexpected results.

Migration 

Middlewares receive the Request object directly and should use it instead of fetching it from $GLOBALS['TYPO3_REQUEST']. Services triggered by middlewares that rely on the Request should have it passed in explicitly. One example frequently used in middlewares is ContentObjectRenderer :

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$cor = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cor->setRequest($request);
$result = $cor->doSomething();
Copied!

Code should in general never rely on the globals _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS. Request-related state should always be fetched from the Request object. Note that the helper method GeneralUtility::getIndpEnv() will also be phased out once the TYPO3 Core has removed its last remaining usages.

Breaking: #108148 - CDATA sections in Fluid templates no longer removed 

See forge#108148

Description 

Previous versions of Fluid and TYPO3 removed code wrapped in <![CDATA[ ]]> from template files altogether. This meant that it was possible to use CDATA to comment out template code. This is no longer possible, since CDATA sections are now interpreted by Fluid in a different way; see Feature: #108148 - Alternative Fluid Syntax for CDATA Sections.

Impact 

<![CDATA[ ]]> can no longer be used to comment out code in Fluid template files.

Affected installations 

Installations that contain Fluid templates using <![CDATA[ ]]> to comment out code are affected. A deprecation has been written to the deprecation log since TYPO3 13.4.21 if this construct is encountered in a Fluid template during rendering.

Migration 

To comment out code in Fluid templates, the Comment ViewHelper <f:comment> should be used. Since TYPO3 v13, potential Fluid syntax errors are ignored by this ViewHelper, which allows commenting out invalid Fluid syntax safely.

Breaking: #108148 - Disallow Fluid variable names with underscore prefix 

See forge#108148

Description 

With Fluid 5, it is no longer possible to define custom template variables that start with an underscore (_). These variable names are reserved for future internal use by Fluid itself, similarly to the already existing {_all}.

Impact 

This change affects ViewHelpers that define new variables, such as:

It also affects Fluid's PHP APIs, namely $view->assign() and $view->assignMultiple().

Affected installations 

Installations with Fluid templates that use custom variable names starting with an underscore (_) will encounter exceptions when such a template is rendered. A deprecation has been written to the deprecation log since TYPO3 13.4.21 if this is encountered in a Fluid template during rendering.

Migration 

The following examples no longer work with Fluid 5:

<f:variable name="_temp" value="a temporary value" />
{_temp}
Copied!
<f:for each="{myArray}" as="_item">
    {_item}
</f:for>
Copied!
<f:render partial="Footer" arguments="{_data: myData}" />
Copied!
$view->assign('_data', $myData);
$view->assignMultiple([
    '_data' => $myData,
]);
Copied!

All examples lead to the following exception:

#1756622558 TYPO3Fluid\Fluid\Core\Variables\InvalidVariableIdentifierException
Variable identifiers cannot start with a "_": _myVariable
Copied!

In all cases, the variable name must be changed to no longer start with an underscore (_).

Note that this only affects variable names, not property names in objects or array keys that are accessed within a Fluid template. The following examples are not affected by this change:

{myArray._myKey}
{myObject._myProperty}
Copied!

Also note that the existing {_all} (and any further internal variables added by Fluid) are not affected. This code will continue to work:

<f:render partial="Footer" arguments="{_all}"/>
Copied!

Breaking: #108148 - Strict Types in Fluid ViewHelpers 

See forge#108148

Description 

With Fluid 5, various changes have been made to use stricter types in the context of ViewHelpers. This has consequences in three areas:

  • Validation of arguments passed to ViewHelpers (see #1194 on GitHub)
  • Passing null values to ViewHelpers that generate a HTML tag, also known as tag-based ViewHelpers (see #1233 on GitHub)
  • Required type declarations for custom ViewHelper implementations (see #1219 on GitHub)

Impact 

ViewHelper argument validation 

Fluid ViewHelpers now use stricter validation for their arguments by default. The previous argument validation had numerous blind spots, which meant that ViewHelper implementations couldn't really rely on the type requirements specified in the ViewHelper's API. The new implementation performs a stricter validation, which means that Fluid might reject arguments passed to ViewHelpers that were previously considered valid (but which the ViewHelper in question usually didn't know how to handle). The new implementation does however deal with simple type conversions automatically, so that a ViewHelper that requires a string still can receive an int as input.

For integrators, this change might reject certain ViewHelper arguments that were previously valid, but not covered by the ViewHelper's specified API.

For developers of custom ViewHelpers, this change allows to get rid of custom validation logic that was previously necessary due to Fluid's spotty validation.

Note that the Argument ViewHelper <f:argument>, which can be used to define an API for a template, is not affected by this change, as it already used the improved validation from the beginning.

Passing null to tag-based ViewHelpers 

Previously, Fluid's TagBuilder class, which is used to create HTML tags in tag-based ViewHelpers, treated null values as empty strings, leading to an HTML tag with an empty HTML attribute. With Fluid 5, null values lead to the HTML attribute being omitted from the resulting HTML tag.

Example:

<f:form.textfield name="myTextBox" placeholder="{variableThatMightBeNull}" />
Copied!

If the variable is null (the PHP value), Fluid 4 and below generated the following output:

<input type="text" name="myTextBox" placeholder="" />
Copied!

Fluid 5 omits the placeholder="":

<input type="text" name="myTextBox" />
Copied!

In most cases, the impact of this change is non-existent. However, there are some edge cases where this change is relevant. In TYPO3 Core, the <f:image> ViewHelper needed to be adjusted to always render the alt attribute, even if its internal value is null, to match the previous output and to produce valid HTML code.

TYPO3 Core ships with the following tag-based ViewHelpers:

  • <f:media> and <f:image>
  • <f:asset.css> and <f:asset.script>
  • <f:form> and <f:form.*>
  • <f:link.*>, except for <f:link.typolink>, which uses TypoScript internally
  • <f:be.link>
  • <be:link.*>
  • <be:thumbnail>

Type declarations in ViewHelper classes 

Fluid's ViewHelperInterface now requires proper return types for all ViewHelper methods. Thus, custom ViewHelper implementations need to be adjusted accordingly. This is backwards-compatible to previous TYPO3 versions.

Affected installations 

All installations need to verify that

  • ViewHelpers aren't called with invalid argument types
  • null values passed to tag-based ViewHelpers don't lead to unexpected HTML output
  • Custom ViewHelper implementations specify proper return types

Migration 

Custom ViewHelper implementations need to make sure that they declare proper return types in the ViewHelper class to conform to Fluid 5's interface changes, for example:

  • initializeArguments() must specify void as return type
  • render() should specify a return type other than void. Even though a specific type is recommended, it is not required, and mixed can be used as well.

Note that properties in ViewHelper classes are not affected. The following example doesn't need to be adjusted, no types can/should be specified for these properties:

class MyViewHelper extends AbstractViewHelper
{
    protected $escapeOutput = false;
    protected $escapeChildren = false;
}
Copied!

Unfortunately, the other changes concern runtime characteristics of Fluid templates, as they depend on the concrete values of variables that are passed to a template. Thus, it is not possible to scan for affected templates automatically.

However, the majority of issues these changes in Fluid might uncover in existing projects would have already been classified as a bug (in the template or extension code) before this Fluid change, such as passing an array to a ViewHelper that expects a string.

Breaking: #108277 - Remove superfluous CacheHashCalculator public methods 

See forge#108277

Description 

The following superfluous public methods have been removed:

  • \TYPO3\CMS\Frontend\Page\CacheHashCalculator::setConfiguration()
  • \TYPO3\CMS\Frontend\Page\CacheHashConfiguration::with()

Impact 

Calling the removed methods will result in a fatal PHP error.

Both methods were only used internally for testing purposes and were not part of the public API contract.

Affected installations 

TYPO3 installations that used these methods directly in custom code are affected. However, this is highly unlikely as they were intended for internal testing only.

Migration 

Instead of modifying configuration after instantiation using the removed setConfiguration() method, merge configuration arrays before creating the CacheHashConfiguration instance, then pass it to the CacheHashCalculator constructor.

Example:

// Before (removed approach):
$subject = new CacheHashCalculator(
    new CacheHashConfiguration($baseConfiguration),
    $hashService
);
$subject->setConfiguration([
    'cachedParametersWhiteList' => ['whitep1', 'whitep2'],
]);

// After (correct approach):
$configuration = new CacheHashConfiguration(
    array_merge($baseConfiguration, [
        'cachedParametersWhiteList' => ['whitep1', 'whitep2'],
    ])
);
$subject = new CacheHashCalculator($configuration, $hashService);
Copied!

Breaking: #108304 - Populate extension title from composer.json 

See forge#108304

Description 

To avoid reading the legacy ext_emconf.php file even in Composer mode, the extension title is now optionally pulled from the composer.json description. If the character sequence - (space, dash, space) is present in description field in composer.json, then everything before this sequence is used as title of the extension and the second part is used as extension description. Note that only the first occurrence of this sequence is evaluated, so it remains possible to have this inside the extension description if required.

Impact 

Extensions not having their title incorporated in the composer.json description field, will be shown with the full description in extension manager and from command line with typo3 extension:list command.

Affected installations 

Installations having custom extensions, where the title is not part of the description in composer.json

Migration 

Put the desired extension title into the composer.json description field and separate it from the description with - (space, dash, space), or use the extension manager "Composer Support of Extensions" to get a suggestion for updating composer.json files accordingly.

All TYPO3 core extensions have set their description in composer.json accordingly already.

Example of description with title in composer.json:

ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'TYPO3 CMS Backend User',
    'description' => 'TYPO3 backend module System>Backend Users for managing backend users and groups.',
    // ...
];
Copied!
composer.json
{
    "name": "typo3/cms-beuser",
    "type": "typo3-cms-framework",
    "description": "TYPO3 CMS Backend User - TYPO3 backend module System>Backend Users for managing backend users and groups.",
}
Copied!

Breaking: #108310 - Require composer.json in classic mode 

See forge#108310

Description 

Extension detection in classic mode now requires a valid composer.json file instead of ext_emconf.php. The composer.json file must include "type": "typo3-cms-*" and the extension key in extra.typo3/cms.extension-key.

Impact 

Extensions without a valid composer.json are no longer detected and loaded in classic mode installations.

Affected installations 

All classic mode installations must verify that every extension contains a composer.json with:

  • "type" starting with "typo3-cms-"
  • "extra.typo3/cms.extension-key" containing the extension key

Composer-based installations are not affected.

Migration 

Extension authors must ensure their extensions include a valid composer.json. TER extensions have required this since 2021.

Example composer.json:

{
    "name": "vendor/extension-name",
    "type": "typo3-cms-extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "extension_name"
        }
    }
}
Copied!

Feature: #92760 - Configurable timezone for DateViewHelper 

See forge#92760

Description 

A new option timezone has been added to the \TYPO3Fluid\Fluid\ViewHelpers\Format\DateViewHelper to render a date with a provided time zone.

<f:format.date format="d.m.Y g:i a" date="1640995200" /><br>
<f:format.date format="d.m.Y g:i a" date="1640995200" timezone="America/Phoenix" /><br>
<f:format.date format="d.m.Y g:i a" date="1640995200" timezone="Indian/Mayotte" />
Copied!

will render:

01.01.2022 12:00 am
31.12.2021 5:00 pm
01.01.2022 3:00 am
Copied!

Impact 

Using the new timezone option, it is now possible to set a specific time zone for the DateViewHelper.

Feature: #93334 - Translation Domain Mapping 

See forge#93334

Description 

Translation domains have been introduced as a shorter alternative to file-based references for label resources (.xlf XLIFF files). The syntax uses the format package[.subdomain...].resource and is fully backward compatible with existing LLL:EXT: references. Package refers to the extension key, such as "backend" for "EXT:backend".

This syntax is designed to improve readability, remove explicit references to file extensions, and provide convenience for new developers and integrators. The previous locallang.xlf convention has been replaced with a more generic "messages" resource name, following common conventions in other localization systems (for example Symfony). This is also where the term translation domain originates.

Example:

// Domain-based reference
$languageService->sL('backend.toolbar:save');

// Equivalent file-based reference (still supported)
$languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:save'
);
Copied!

Translation Domain Format 

The format defines two parts: the package part (extension key) and the resource part, separated by a dot.

The resource part omits historical namings such as locallang.xlf and the locallang_ prefix. The actual label identifier is separated by a colon.

Format 

Example usage of "package.resource:identifier"
$languageService->sL('backend.toolbar:save');
// Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf
// and returns the translated "save" identifier.
Copied!

Domain Resolution 

Deterministic File-Based Mapping 

Translation domains are resolved deterministically by scanning the file system. When a domain is first requested for a package:

  1. All label files in Resources/Private/Language/ are discovered.
  2. A domain name is generated from each file name.
  3. The domain-to-file mapping is cached in cache.l10n.
  4. Subsequent requests use the cached mapping.

This ensures that domain names always correspond to existing files and avoids speculative file system lookups.

When there are filename conflicts such as locallang_db.xlf and db.xlf, then locallang_db.xlf will be ignored.

Performance Characteristics 

The implementation reduces file system operations compared to traditional file-based lookups, as all label files within an extension are discovered once.

Domain Generation Rules 

Domain names are generated from file paths using these transformation rules:

  1. The base path Resources/Private/Language/ is omitted.
  2. Standard filename patterns:

    • locallang.xlf.messages
    • locallang_toolbar.xlf.toolbar
    • locallang_sudo_mode.xlf.sudo_mode
  3. Subdirectories use dot notation:

    • Form/locallang_tabs.xlf.form.tabs
  4. Site Set labels receive the .sets prefix:

    • Configuration/Sets/Felogin/labels.xlf.sets.felogin
  5. Case conversion:

    • UpperCamelCase → snake_case (SudoModesudo_mode)
    • snake_case → preserved (sudo_modesudo_mode)
  6. Locale prefixes are ignored for domain name generation but properly evaluated for locale-specific translations:

    • (de.locallang.xlfmessages)
    • (de-AT.tabs.xlftabs)

Examples:

File Path                                     → Domain
────────────────────────────────────────────────────────────
EXT:backend/.../locallang.xlf                 → backend.messages
EXT:backend/.../locallang_toolbar.xlf         → backend.toolbar
EXT:core/.../Form/locallang_tabs.xlf          → core.form.tabs
EXT:felogin/Configuration/Sets/.../labels.xlf → felogin.sets.felogin
Copied!

Usage 

The translation domain system integrates with the existing \TYPO3\CMS\Core\Localization\LanguageService API. Both domain-based and file-based references are supported:

use TYPO3\CMS\Core\Localization\LanguageService;

$languageService = $this->languageServiceFactory->createFromSiteLanguage(
    $request->getAttribute('language')
);

// Domain-based reference
$label = $languageService->sL('backend.toolbar:menu.item');

// Another domain-based reference
$label = $languageService->sL('backend.messages:button.save');

// Traditional file reference (still supported)
$label = $languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:button.save'
);
Copied!

Domain-based references are shorter and reveal less implementation detail than full file paths.

CLI Command 

The development command bin/typo3 language:domain:list lists all available translation domains along with their available translations and label counts:

# List domains in active extensions
php bin/typo3 language:domain:list

# Filter by extension
php bin/typo3 language:domain:list --extension=backend
Copied!

Output:

+--------------------+---------------------------------------+----------+
| Translation Domain | Label Resource                        | # Labels |
+--------------------+---------------------------------------+----------+
| backend.messages   | EXT:backend/.../locallang.xlf         | 84       |
| backend.toolbar    | EXT:backend/.../locallang_toolbar.xlf | 42       |
+--------------------+---------------------------------------+----------+
Copied!

The Labels column displays the number of translatable labels within the English source file.

On top of this, the development command bin/typo3 language:domain:search can be used to search for specific label contents. Both commands are provided in the EXT:lowlevel extension.

PSR-14 Event 

The event \TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent is dispatched after domain generation, allowing customization of domain names.

The event provides these public properties:

  • $packageKey — The extension key (read-only).
  • $domains — An associative array mapping domain names to label files (modifiable): array<string, string>.

Example 

Event listener implementation:

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent;

final readonly class CustomTranslationDomainResolver
{
    #[AsEventListener(identifier: 'my-extension/custom-domain-names')]
    public function __invoke(BeforeLabelResourceResolvedEvent $event): void
    {
        if ($event->packageKey !== 'my_extension') {
            return;
        }

        // Use file my_messages.xlf even if locallang.xlf is found
        $event->domains['my_extension.messages'] =
            'EXT:my_extension/Resources/Private/Language/my_messages.xlf';
    }
}
Copied!

Backend modules 

Previously, backend module labels (including their title and description) were defined in a file like this:

EXT:my_extension/Resources/Private/Language/locallang_mod.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="EXT:my_extension/Resources/Private/Language/locallang_mod.xlf" date="2038-10-28T13:37:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="mlang_labels_tablabel">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="mlang_labels_tabdescr">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="mlang_tabs_tab">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

and utilized via the module definition:

EXT:my_extension/Configuration/Backend/Modules.php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

Now, labels can use more speaking identifiers:

EXT:my_extension/Resources/Private/Language/Module/mymodule.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="EXT:my_extension/Resources/Private/Language/Modules/mymodule.xlf" date="2026-11-05T16:22:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="short_description">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="description">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="title">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!
EXT:my_extension/Configuration/Backend/Modules.php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'my_extension.modules.my_module',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The naming for the short-hand translation domain for modules should follow the following pattern as best practice:

  • <extensionkey>.modules.<modulename> - when multiple modules exist for an extension. Both extensionKey and modulename should use lower snake case ("some_long_module_name"), ideally without underscores (qrcode.modules.generator is more readable than qrcode.modules.backend_image_generator for example). Files are put into EXT:extensionkey/Resources/Private/Languages/Modules/modulename.xlf.
  • <extensionkey>.module - single backend module only The file is saved as EXT:extensionkey/Resources/Private/Languages/module.xlf.

To summarize, the key changes are:

  1. Use a speaking XLIFF file inside /Resources/Private/Languages/Modules (best practice, could be any sub-directory)
  2. Use understandable XLIFF identifiers: - "title" instead of "mlang_tabs_tab" - "short_description" instead of "mlang_labels_tablabel" - "description" instead of "mlang_labels_tabdescr"
  3. Use short-form identifiers ("my_extension.modules.my_module" instead of "LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf") inside the Backend/Modules.php registration.

All TYPO3 Core backend modules that used the old label identifiers have been migrated to the new syntax, the utilized files are now deprecated, see deprecation. TYPO3 Core also uses singular module language containers like workspaces.module instead of workspaces.modules.workspaces.

Impact 

Translation domains provide a shorter, more readable alternative to file-based label references. The implementation uses deterministic file-system scanning with per-package caching to reduce lookups.

All existing LLL:EXT: file references continue to work. Translation domains are optional and can be adopted incrementally. Both syntaxes can coexist in the same codebase. This affects TypoScript, Fluid <f:translate> usages, TCA configuration, and PHP code using the LanguageService API.

TYPO3 Core will gradually migrate internal references to translation domains over time, increasing readability—especially in Fluid templates or TCA definitions.

Technical components:

\TYPO3\CMS\Core\Localization\TranslationDomainMapper
Maps domains to file paths and manages the cache.
\TYPO3\CMS\Core\Localization\LabelFileResolver
Discovers label files and handles locale resolution.
\TYPO3\CMS\Core\Localization\LocalizationFactory
Integrates domain resolution transparently.

The TranslationDomainMapper automatically detects EXT: file references and passes them through unchanged.

Feature: #93981 - Specify default image conversion processing 

See forge#93981

Description 

Image processing in TYPO3 can now configure default and specific formats to use when images are rendered or converted in the frontend, for example in Fluid:

Example of image rendering in Fluid
<f:image src="{asset.path}" width="200" />
<f:image src="fileadmin/someFile.jpg" width="200" />
<f:image image="{someExtbaseObject.someAsset}" width="200" />
Copied!

Depending on the TYPO3 version, processed images were rendered with .png (or earlier, .gif or .png) file extensions, as long as the fileExtension parameter with a fixed output format was not specified.

This default solution had two major drawbacks:

  1. The default file format was hardcoded and not configurable.
  2. Utilizing new file formats (like webp and avif) required code changes.

This has now been changed with the new configuration option $GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] .

This variable defaults to:

$GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] = [
    'jpg' => 'jpg',
    'jpeg' => 'jpeg',
    'gif' => 'gif',
    'png' => 'png',
    'svg' => 'svg',
    'default' => 'png',
];
Copied!

This means:

  • When resizing or cropping an image with a file extension of jpg, jpeg, gif, png, or svg (and not setting a specific fileExtension target format), those images will retain their respective file formats.
  • Otherwise, the file format png is used.

Related configuration options that still apply:

  • $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] Current default: gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg,webp,avif
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] Current default: txt,ts,typoscript,html,htm,css,tmpl,js,sql,xml,csv,xlf,yaml,yml
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] Current default: gif,jpg,jpeg,bmp,png,webp,pdf,svg,ai,mp3,wav,mp4,ogg,flac,opus,webm, youtube,vimeo,avif

These still define, per installation, which files can be uploaded or used as images.

If a new format like heic becomes supported by the used graphics engine (for example GraphicsMagick or ImageMagick), system maintainers can add the file extension to imagefile_ext. TYPO3 will then recognize this format and convert it to the default target format (png by default) when processing images.

If the format should also be available as a target format, add 'heic' => 'heic' to $GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] . Otherwise, heic images can be selected, but processing will still produce png.

If all image processing (resizing, thumbnails, PDF previews, etc.) should use heic, set 'default' => 'heic' and remove other entries.

Currently, the option cannot be configured via System > Settings > Configure options ... because it uses an array syntax. It can be changed manually in settings.php. GUI support may be added in a future release.

The array notation allows defining a target (thumbnail) file extension for each original file extension individually using the format {originalExtension} => {targetExtension}.

Example:

Individual thumbnail formats per file extension
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] = [
    'jpg' => 'jpg',
    'svg' => 'svg',
    'ai' => 'png',
    'heic' => 'webp',
    'default' => 'avif',
];
Copied!

This configuration would produce the following behavior:

Fluid image rendering results
 <f:image src="somefile.jpg" width="80">
  -> renders an 80 px thumbnail from "somefile.jpg" to "somefile.jpg"
     (rule: "jpg => jpg")

<f:image src="somefile.gif" width="80">
  -> renders an 80 px thumbnail from "somefile.gif" to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.png" width="80">
  -> renders an 80 px thumbnail from "somefile.png" to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.svg" width="80">
  -> renders the original SVG at 80 px width
     (rule: "svg => svg")

<f:image src="somefile.pdf" width="80">
  -> renders an 80 px PDF thumbnail to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.heic" width="80">
  -> renders an 80 px thumbnail from "somefile.heic" to "somefile.webp"
     (rule: "heic => webp")
Copied!

Impact 

TYPO3 is now more future-proof regarding new image formats and allows modern file formats to be used by configuration.

Projects can now specify precisely how each format should be converted or which default format should be used for processed images.

Feature: #97559 - Support property-based configuration for Extbase attributes 

See forge#97559

Description 

PHP attributes in the Extbase context can now be configured using properties instead of an array of configuration values. This resolves a limitation that existed since the introduction of Extbase annotations in TYPO3 v9, where annotation configuration was restricted: all available options needed to be defined in a single array.

Since annotations were removed with forge#107229 in favor of PHP attributes, configuration options can now be defined in a more flexible and type-safe way.

Example usage 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(validator: 'NotEmpty')]
    protected string $foo = '';

    #[FileUpload(
        validation: [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        uploadFolder: '1:/user_upload/files/',
    )]
    protected ?FileReference $bar = null;
}
Copied!

Impact 

This patch serves as a follow-up to forge#107229 and aims to improve the attribute configuration mechanism by using constructor property promotion in combination with strictly typed properties.

To maintain backwards compatibility, the first property of each attribute still accepts an array of configuration options. However, this behavior is deprecated and will be removed in TYPO3 v15.0 (see deprecation notice).

Developers are advised to migrate to single-property configuration when using PHP attributes in Extbase.

Feature: #98239 - PSR-14 Event to modify form after being built 

See forge#98239

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent has been introduced which serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'] .

The event provides the $form public property.

Example 

An example event listener could look like the following:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent;

class MyEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/after-form-is-built',
    )]
    public function __invoke(AfterFormIsBuiltEvent $event): void
    {
        $event->form->setLabel('foo');
    }
}
Copied!

Impact 

With the new AfterFormIsBuiltEvent, it is now possible to modify the form definition after it has been built.

Feature: #99065 - Detail view for backend user groups in 'Users' module 

See forge#99065

Description 

The Administration > Users module has been extended with a new detail view for backend user groups, complementing the existing detail view for individual backend users. This comprehensive view provides administrators with complete visibility into backend user group configurations and their calculated properties.

The detail view displays:

  • Basic information about the backend user group (title, description, and so on)
  • All assigned subgroups and the full inheritance chain
  • A complete overview of permissions and access rights from the group and all inherited subgroups
  • Calculated and processed TSconfig settings showing the final effective configuration
  • Database mount points and file mount access permissions
  • Module access permissions and workspace restrictions

Impact 

TYPO3 administrators can now efficiently analyze backend user group configurations without manually tracing through complex inheritance structures. This enhanced visibility simplifies permission troubleshooting, security auditing, and group management by providing a consolidated view of all calculated permissions and settings in one place, similar to the existing backend user detail functionality.

Feature: #99409 - New PSR-14 BeforeLiveSearchFormIsBuiltEvent 

See forge#99409

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent has been added.

To modify the live search form data, the following methods are available:

  • addHint(): Adds a single hint.
  • addHints(): Adds one or multiple hints.
  • setHints(): Sets hints and can be used to reset or overwrite the current ones.
  • getHints(): Returns all current hints.
  • getRequest(): Returns the current PSR-7 request.
  • getSearchDemand(): Returns the SearchDemand used by the live search.
  • setSearchDemand(): Sets a custom SearchDemand object.
  • getAdditionalViewData(): Returns the additional view data set to be used in the template.
  • setAdditionalViewData(): Sets the additional view data to be used in the template.

Example 

The corresponding event listener class:

<?php

namespace MyVendor\MyPackage\Backend\Search\EventListener;

use TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class BeforeLiveSearchFormIsBuiltEventListener
{
    #[AsEventListener(
        identifier: 'my-package/backend/search/modify-live-search-form-data'
    )]
    public function __invoke(BeforeLiveSearchFormIsBuiltEvent $event): void
    {
        $event->addHints(...[
            'my-extension.messages:identifier',
        ]);
    }
}
Copied!

Impact 

With the new PSR-14 event BeforeLiveSearchFormIsBuiltEvent, it is now possible to modify the form data for the backend live search.

Feature: #99459 - Respect record type while creating new records 

See forge#99459

Description 

The "Create new record" component in the backend, which is accessible in the Content > List module, has been enhanced to automatically detect and display all available record types for tables that support sub-schemas (record types). This improvement simplifies creating specific record types and eliminates the need to change the type afterward in the editing form, which could previously lead to invalid record states being stored in the database.

The interface now features automatic record type detection. Tables with multiple record types are automatically expanded to show all available types in a collapsible dropdown interface.

The options to create new pages now also show the different page types ( doktype) as expandable options when creating new pages "inside" or "after" an existing page.

To disable direct creation of a specific record type, a new TCA option ['creationOptions']['enableDirectRecordTypeCreation'] is available at the record type level:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
use TYPO3\CMS\Core\Domain\Repository\PageRepository´;

// Disable direct creation of shortcuts
$GLOBALS['TCA']['pages']['types'][(string)PageRepository::DOKTYPE_SHORTCUT]['creationOptions']['enableDirectRecordTypeCreation'] = false;
Copied!

Individual titles for each record type are automatically picked up from the type-specific title configuration in the types section (see Feature #108027), falling back to the select item label if no type-specific title is defined.

Additionally, the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyNewRecordCreationLinksEvent allows for complete customization of the creation links.

Impact 

For tables that support sub-schemas (multiple record types), the new record wizard automatically detects all available types and displays them in a collapsible interface. This includes:

  • All tables with TCA type fields (such as sys_file_collection or index_config)
  • The pages table with its different doktype values
  • Extension tables with custom record types

The ModifyNewRecordCreationLinksEvent provides complete control over the creation link structure, allowing extensions to:

  • Add custom record creation options
  • Modify existing groups and items
  • Override icons, labels, and URLs
  • Create entirely custom wizard interfaces

Data Structure 

The event works with a nested array structure representing grouped creation links:

[
    'content' => [
        'title' => 'Content',
        'icon' => '<img src="..." />',
        'items' => [
            'sys_file_collection' => [
                'label' => 'File Collection',
                'icon' => '<typo3-backend-icon ...>',
                'types' => [
                    'static' => [
                        'url' => '/typo3/record/edit?edit[sys_file_collection][1]=new&defVals[sys_file_collection][type]=static',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Static File Collection'
                    ],
                    'folder' => [
                        'url' => '/typo3/record/edit?edit[sys_file_collection][1]=new&defVals[sys_file_collection][type]=folder',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Folder from Storage'
                    ]
                ]
            ]
        ]
    ],
    'pages' => [
        'title' => 'Create New Page',
        'icon' => '<typo3-backend-icon ...>',
        'items' => [
            'inside' => [
                'label' => 'Page (inside)',
                'icon' => '<typo3-backend-icon ...>',
                'types' => [
                    '1' => [
                        'url' => '/typo3/record/edit?edit[pages][1]=new&defVals[pages][doktype]=1',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Standard Page'
                    ],
                    '254' => [
                        'url' => '/typo3/record/edit?edit[pages][1]=new&defVals[pages][doktype]=254',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Folder'
                    ]
                ]
            ]
        ]
    ]
]
Copied!

Event Listener Example 

The event provides access to:

  • $event->groupedCreationLinks - The complete structure of creation links
  • $event->pageTS - The current page's TSconfig array
  • $event->pageId - The current page ID
  • $event->request - The current server request object

This allows for comprehensive customization while maintaining backward compatibility with existing setups.

EXT:my_extension/Classes/EventListener/CustomizeNewRecordCreationLinksEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyNewRecordCreationLinksEvent;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;

final readonly class CustomizeNewRecordWizardEventListener
{
    public function __construct(
        private IconFactory $iconFactory,
        private UriBuilder $uriBuilder,
    ) {}

    #[AsEventListener]
    public function __invoke(ModifyNewRecordCreationLinksEvent $event): void
    {
        // Add a custom creation group
        $customGroup = [
            'title' => 'Custom Records',
            'icon' => $this->iconFactory->getIcon('apps-pagetree-category')->render(),
            'items' => [
                'tx_myext_domain_model_item' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['tx_myext_domain_model_item' => [$event->pageId => 'new']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIconForRecord('tx_myext_domain_model_item', []),
                    'label' => 'Custom Item',
                ]
            ]
        ];

        // Add the custom group to the existing structure
        $event->groupedCreationLinks['custom'] = $customGroup;

        // Modify existing groups, for example, remove specific items
        if (isset($event->groupedCreationLinks['system']['items']['sys_template'])) {
            unset($event->groupedCreationLinks['system']['items']['sys_template']);
        }

        // Add custom types to an existing table
        if (isset($event->groupedCreationLinks['content']['items']['sys_note'])) {
            $event->groupedCreationLinks['content']['items']['sys_note']['types'] = [
                'important' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['sys_note' => [$event->pageId => 'new']],
                        'defVals' => ['sys_note' => ['category' => '1']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIcon('status-dialog-warning', IconSize::SMALL),
                    'label' => 'Important Note',
                ],
                'info' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['sys_note' => [$event->pageId => 'new']],
                        'defVals' => ['sys_note' => ['category' => '0']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIcon('status-dialog-information', IconSize::SMALL),
                    'label' => 'Information Note',
                ]
            ];
        }
    }
}
Copied!

Feature: #99911 - New TCA type "country" 

See forge#99911

Description 

A new TCA field type called country has been added to TYPO3 Core. Its main purpose is to use the newly introduced Country API to provide a country selection in the backend and use the stored representation in Extbase or TypoScript output.

TCA Configuration 

The new TCA type displays all filtered countries including the configurable name and the corresponding flag.

Configuration/TCA/tx_myextension_mymodel.php
'country' => [
    'label' => 'Country',
    'config' => [
        'type' => 'country',
        // available options: name, localizedName, officialName, localizedOfficialName, iso2, iso3
        'labelField' => 'localizedName',
        // countries which are listed before all others
        'prioritizedCountries' => ['AT', 'CH'],
        // sort by the label
        'sortItems' => [
            'label' => 'asc'
        ],
        'filter' => [
            // restrict to the given country ISO2 or ISO3 codes
            'onlyCountries' => ['DE', 'AT', 'CH', 'FR', 'IT', 'HU', 'US', 'GR', 'ES'],
            // exclude by the given country ISO2 or ISO3 codes
            'excludeCountries' => ['DE', 'ES'],
        ],
        'default' => 'HU',
        // When required=false, an empty selection ('') is possible
        'required' => false,
    ],
],
Copied!

Note that extra items / countries should be added via the new PSR-14 event BeforeCountriesEvaluatedEvent.

FlexForm Configuration 

Similar keys work for FlexForms:

Configuration/FlexForms/example.xml
<settings.country>
    <label>My Label</label>
    <config>
        <type>country</type>
        <labelField>officialName</labelField>
        <prioritizedCountries>
            <numIndex index="0">AT</numIndex>
            <numIndex index="1">CH</numIndex>
        </prioritizedCountries>
        <filter>
            <onlyCountries>
                <numIndex index="0">DE</numIndex>
                <numIndex index="1">AT</numIndex>
                <numIndex index="2">CH</numIndex>
                <numIndex index="1">FR</numIndex>
                <numIndex index="3">IT</numIndex>
                <numIndex index="4">HU</numIndex>
                <numIndex index="5">US</numIndex>
                <numIndex index="6">GR</numIndex>
                <numIndex index="7">ES</numIndex>
            </onlyCountries>
            <excludeCountries>
                <numIndex index="0">DE</numIndex>
                <numIndex index="1">ES</numIndex>
            </excludeCountries>
        </filter>
        <sortItems>
            <label>asc</label>
        </sortItems>
        <default>HU</default>
        <required>1</required>
    </config>
</settings.country>
Copied!

Available config keys 

The TCA type country features the following column configuration:

  • filter (array): onlyCountries (array), excludeCountries (array) - filter/reduce specific countries
  • prioritizedCountries (array) - items put first in the list
  • default (string) - default value
  • labelField (string) - display label (one of localizedName, name, iso2, iso3, officialName, localizedOfficialName)
  • sortItems (string) - sort order (asc, desc)
  • required (bool) - whether an empty selection can be made or not

Extbase usage 

When using Extbase Controllers to fetch Domain Models containing properties declared with the Country type, these models can be used with their usual getters, and passed along to Fluid templates as usual.

Extbase Domain Model example
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Core\Country\Country;

class SomeDomainModel extends AbstractEntity
{
    protected ?Country $country = null;

    public function setCountry(?Country $country): void
    {
        $this->country = $country;
    }

    public function getCountry(): ?Country
    {
        return $this->country;
    }
}
Copied!
Extbase Controller usage
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ItemController extends ActionController {
    // ...

    public function __construct(
        private readonly CountryProvider $countryProvider,
    ) {}

    public function singleAction(SomeDomainModel $model): ResponseInterface
    {
        // Do something in PHP, using the Country API
        if ($model->getCountry()->getAlpha2IsoCode() == 'DE') {
            $this->loadGermanLanguage();
        }
        $this->view->assign('model', $model);

        // You can access the `CountryProvider` API for additional country-related
        // operations, too (ideally use Dependency Injection for this):
        $this->view->assign('countries', $this->countryProvider->getAll());

        return $this->htmlResponse();
    }
}
Copied!
Fluid Template example
Country: {model.country.flag}
 - <span title="{f:translate(key: model.country.localizedOfficialNameLabel)}">
     {model.country.alpha2IsoCode}
   </span>
Copied!

You can use any of the getXXX() methods available from the Country API via the Fluid {model.country.XXX} accessors.

If you use common Extbase CRUD (Create/Read/Update/Delete) with models using a Country type, you can utilize the existing ViewHelper f:form.countrySelect within your <f:form> logic.

Please keep in mind that Extbase by default has no coupling (in terms of validation) to definitions made in the TCA for the properties, as with other types like file uploads or select items.

That means, if you restrict the allowed countries via filter.onlyCountries on the backend (TCA) side, you also need to enforce this in the frontend.

It is recommended to use Extbase Validators for this task. If you want to share frontend-based validation and TCA-based validation non-redundantly, you could use data objects (DO/DTO) or ENUMs for returning the list of allowed countries:

EXT:my_extension/Classes/Domain/Validator/CountryValidator.php
namespace MyExtension\Domain\Validator;

use TYPO3\CMS\Extbase\Validation\Error;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

class ItemValidator extends AbstractValidator
{
    /**
     * @param MyModel $value
     */
    protected function isValid(mixed $value): void
    {
        if ($value->getCountry() === null) {
            $error = new Error('Valid country (alpha2) must be set.', 4815162343);
            $this->result->forProperty('country')->addError($error);
        } else {
            $allowedCountries = ['DE', 'EN'];
            if (!in_array($value->getCountry()->getAlpha2IsoCode(), $allowedCountries)) {
                $error = new Error('Country ' . $value->getCountry()->getAlpha2IsoCode() . ' not allowed.', 4815162344);
                $this->result->forProperty('country')->addError($error);
            }
        }
    }
}
Copied!
EXT:my_extension/Classes/Controller/ItemController.php (excerpt)
namespace MyExtension\Controller;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use MyExtension\Domain\Model\Item;
use MyExtension\Domain\Validator\ItemValidator;

final class ItemController extends ActionController
{
    // Excerpt ...

    #[Validate([
        'param' => 'item',
        'validator' => CountryValidator::class,
    ])]
    public function createAction(Item $item): ResponseInterface
    {
        $this->itemRepository->add($item);
        return $this->htmlResponse();
    }

    // ...
}
Copied!

A full working example, including an Extbase CRUD setup, can be found in EXT:tca_country_example Demo Extension.

Extbase / Fluid localization 

The type Country does not point to a real Extbase model, and thus has no inherent localization or query logic based on real records. It is just a pure PHP data object with some getters, and a magic __toString() method returning a language label (LLL:...) translation key for the name of the country ( Country->getLocalizedNameLabel()).

Here are some examples how to access them and provide localization:

EXT:my_extension/Resources/Private/Templates/Show.html
<f:comment>Will show something like "AT" or "DE"</f:comment>
Country ISO2:
    {item.country.alpha2IsoCode}

<f:comment>Will show something like "CHE"</f:comment>
Country ISO3:
    {item.country.alpha3IsoCode}

<f:comment>Will show something a flag (UTF-8 character)</f:comment>
Country flag:
    {item.country.flag}

<f:comment>Will show something like "LLL:EXT:core/Resources/Private/Language/Iso/countries.xlf:AT.name"</f:comment>
Country LLL label:
    {item.country}
Actual localized country:
    <f:translate key="{item.country}" />

<f:comment>Will show something like "LLL:EXT:core/Resources/Private/Language/Iso/countries.xlf:AT.official_name"</f:comment>
Country LLL label:
    {item.country.localizedOfficialNameLabel}
Actual localized official country name:
    <f:translate key="{item.country.localizedOfficialNameLabel}" />

<f:comment>Will show something like "Germany" (always English)</f:comment>
    {item.country.name}
Copied!

You can use the Extbase \TYPO3\CMS\Extbase\Utility\LocalizationUtility in PHP scope (Controllers, Domain Model) to create a custom getter in your Domain Model to create a shorthand method:

EXT:my_extension/Domain/Model/Item.php
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Core\Country\Country;

class SomeDomainModel extends AbstractEntity
{
    protected ?Country $country = null;

    public function setCountry(?Country $country): void
    {
        $this->country = $country;
    }

    public function getCountry(): ?Country
    {
        return $this->country;
    }

    // Special getter to easily access `{item.localizedCountry}` in Fluid
    public function getLocalizedCountry(): string
    {
        return (string) LocalizationUtility::translate(
            (string) $this->getCountry()?->getLocalizedNameLabel()
        );
    }
}
Copied!

Extbase Repository access 

As mentioned above, since Country has no database-record relations. The single-country relation always uses the 2-letter ISO alpha2 key (respectively custom country keys, when added via the PSR-14 event BeforeCountriesEvaluatedEvent ). Thus, queries need to utilize them as string comparisons:

EXT:my_extension/Classes/Domain/Repository/ItemRepository.php
namespace MyExtension\Domain\Repository;

use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;

class ItemRepository extends Repository
{
    public function findByGermanMarkets(): QueryResultInterface {
        $query = $this->createQuery();
        $query->matching(
            $query->in('country', ['DE', 'AT', 'CH'])
        );
        return $query->execute();
    }
}
Copied!

The default Extbase repository magic method $repository->findBy(['country' => 'DE']) will work, too.

TypoScript rendering usage via record-transformation 

Database records using 'country' type fields can be rendered with the TypoScript-based record-transformation rendering (data processor).

You can specify how a field containing a country is rendered in the output (using the name, the flag icon, specific ISO keys) with regular Fluid logic then:

Step 1: TypoScript utilizing record-transformation, defining a Homepage.html Fluid template
page = PAGE
page {
  # Just an example basic template for your site. The important section starts with `dataProcessing`!
  100 = FLUIDTEMPLATE
  100 {
    templateName = Homepage
    templateRootPaths {
      0 = EXT:myextension/Resources/Private/Templates/
    }
    dataProcessing {
      10 = database-query
      10 {
        as = mainContent
        # This table holds for example a TCA type=country definition for a field "country"
        table = tx_myextension_domain_model_mycountries
        # An extra boolean field "show_on_home_page" would indicate whether these
        # records are fetched and displayed on the home page
        where = show_on_home_page=1
        # Depending on your table storage you may need to set a proper pidInList constraint.
        #pidInList = 4711
        dataProcessing {
          # Makes all records available as `{mainContent.[0..].myRecord}` in the
          # Fluid file EXT:myextension/Resources/Private/Templates/Homepage.html
          10 = record-transformation
          10 {
            as = myRecord
          }
        }
      }
    }
  }
}
Copied!
Step 2: Fluid template EXT:myextension/Resources/Private/Templates/Homepage.html
<f:if condition="{mainContent}">
  <f:for each="{mainContent}" as="element">
    <!-- given that your 'tx_myextension_domain_model_mycountries' has a TCA field called "storeCountry":
    Selected Country:
      <f:translate key="{element.myRecord.storeCountry.localizedOfficialNameLabel}" />
  </f:for>

  <!-- note that you can access any transformed record type object via 'element', also multiple country
       elements could be contained in 'element.myRecord'. -->
</f:if>
Copied!

Impact 

It is now possible to use a dedicated TCA type for storing a relation to a country in a record.

Using the new TCA type, corresponding database columns are added automatically. Country-annotated properties of Extbase Domain Models can be evaluated in Extbase and via TypoScript.

Feature: #101059 - Allow install tool sessions without shared file system 

See forge#101059

Description 

It is now possible to store Install Tool sessions in Redis or configure the session storage path for file-based Install Tool sessions.

As a shipped session handler, Redis can now be configured via options in $GLOBALS['TYPO3_CONF_VARS'] for host, port, database, and authentication.

Example 

To configure an alternative session handler for Install Tool sessions, set the required options in your settings.php or additional.php file:

File-based session handler in config/system/settings.php
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Install\Service\Session\FileSessionHandler;

return [
    'BE' => [
        'installToolSessionHandler' => [
            'className' => FileSessionHandler::class,
            'options' => [
                'sessionPath' => Environment::getVarPath() . '/session',
            ],
        ],
    ],
];
Copied!
Redis session handler in config/system/settings.php
use TYPO3\CMS\Install\Service\Session\RedisSessionHandler;

return [
    'BE' => [
        'installToolSessionHandler' => [
            'className' => RedisSessionHandler::class,
            'options' => [
                'host' => '127.0.0.1',
                'port' => 6379,
                'database' => 0,
                'authentication' => [
                    'user' => 'redis',
                    'pass' => 'redis',
                ],
            ],
        ],
    ],
];
Copied!

Impact 

The default file-based session handling for the Install Tool remains unchanged. If no alternative session handler for the Install Tool is configured, the default behavior is used.

Custom session handlers can be created by implementing PHP's \SessionHandlerInterface.

Feature: #103258 - Language filter for list module 

See forge#103258

Description 

The list module now provides a language filter in the document header, similar to the existing language selector in the page module. This allows backend users to filter records by language, making it easier to focus on content in a specific language when working with multilingual websites.

The language filter appears as a dropdown button in the document header toolbar and provides the following options:

  • All available site languages that have page translations
  • "All languages" (if translations exist on the current page)

Key behaviors 

Language selection persistence 

The selected language is stored in the backend user's module data and persists across page navigation. When switching between pages, the previously selected language remains active if available on the new page.

Automatic fallback 

If navigating to a page where the selected language is not available (no translation exists), the list module automatically falls back to:

  • "All languages" mode (if the page has any translations)
  • The default language (if the page has no translations at all)

This fallback is temporary and does not overwrite the language preference. When navigating to a page with the selected language translation, it will automatically be restored.

Display behavior 

When a specific language is selected:

  • Records in the selected language are displayed
  • Default language records (language 0) are always included as fallback
  • Records with the "all languages" flag (-1) are included

Example: If French is selected, one will see French translations, default language content, and content marked for "all languages".

Localization restrictions 

The localization panel respects page translation availability. When a language is selected or when viewing in "all languages" mode, the list module only offers localization options for languages where the page has an existing translation.

This ensures data integrity by preventing the creation of records in languages where the parent page does not exist.

Impact 

Backend users can now efficiently filter list module records by language, improving the workflow when managing multilingual content.

The language selection persists across page navigation, reducing the need to repeatedly select the same language. The intelligent fallback mechanism ensures the list module always displays relevant content, even when switching between pages with different translation availability.

This enhancement brings the list module's language handling in line with that of the page module, providing a consistent user experience across TYPO3's backend.

Feature: #103740 - Language selection for backend module "Status - Pagetree Overview" 

See forge#103740

Description 

The backend module Content > Status > Pagetree Overview has been enhanced with a language selection option.

This change makes it possible to switch the displayed page tree to the selected language and adjust all labels, as well as edit and view links, accordingly.

The language selection dropdown is located next to the other filter options (recursion depth, information type) and complements the Content > Status > Localization Overview module by providing a page- and record-focused view.

Impact 

The Content > Status backend module is now more useful for sites with multiple languages, offering a quick overview of information for the selected page and its subpages in the chosen language.

Feature: #104054 - Add cache flush tags command 

See forge#104054

Description 

A new command cache:flushtags has been introduced to allow flushing cache entries by tag.

Multiple tags can be flushed by passing a comma-separated list of tags. It is also possible to flush tags for a specific cache group by using the --groups or -g option. If no group is specified, all cache groups are considered.

Note that certain combinations of groups and tags do not make sense, specifically the di and system cache groups.

Examples 

Example command usage (Composer mode projects)
vendor/bin/typo3 cache:flushtags pageId_123
vendor/bin/typo3 cache:flushtags pages_100,pages_200
vendor/bin/typo3 cache:flushtags tx_news -g pages
Copied!

Impact 

It is now possible to flush cache entries for specific tag and group combinations directly from the command line.

Feature: #104058 - Introduce install:password:set command 

See forge#104058

Description 

The Install Tool password can now also be set via the TYPO3 command line interface instead of only via direct file access. This allows for better automation and easier hash generation without manual steps.

Usage 

Interactively create and write an Install Tool password hash:

vendor/bin/typo3 install:password:set
Copied!

Return the generated password hash without writing to the configuration:

vendor/bin/typo3 install:password:set --dry-run
Copied!

Run without interaction (generates a random password):

vendor/bin/typo3 install:password:set --no-interaction
Copied!

Options can be combined as needed:

vendor/bin/typo3 install:password:set --dry-run --no-interaction
Copied!

The last variation can, for example, be used in CI/CD automation to generate a suitable password, process the output (the generated password needs to be persisted separately), and store it in vaults or environment variables for later use.

Impact 

It is now possible to set the hashed Install Tool password using the TYPO3 command line interface.

Feature: #105549 - Support qualified and unqualified ISO8601 dates in DataHandler 

See forge#105549

Description 

The DataHandler API has been extended to support both qualified and unqualified ISO 8601 date formats, correctly handling supplied timezone offsets when provided.

Qualified ISO 8601
Includes an explicit timezone offset (for example, 1999-12-11T10:09:00+01:00 or 1999-12-11T10:09:00Z)
Unqualified ISO 8601
Omits timezone offsets, representing LOCALTIME (for example, 1999-12-11T10:09:00)

The DataHandler now accepts five different formats:

Format Examples
Unqualified ISO 8601 (LOCALTIME) 'Y-m-d\\TH:i:s' 1999-11-11T11:11:11
Qualified ISO 8601 'Y-m-d\\TH:i:sP'

1999-11-11T10:11:11Z

1999-11-11T11:11:11+01:00

DateTime objects \DateTimeInterface

new \DateTime('yesterday')

new \DateTimeImmutable()

SQL-flavored dates (internal use) 'Y-m-d H:i:s' 1999-11-11 11:11:11
Unix timestamps (internal use) 'U' 942315071

The ISO 8601 variants and \DateTimeInterface objects are intended for use in the public API. The SQL-flavored variant and Unix timestamps are primarily intended for internal operations such as copy or import processes involving native DATETIME and INT timestamp database fields.

Passing datetime data via the DataHandler PHP API
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // Format as LOCALTIME
            'mydatefield_1' => $myDate->format('Y-m-d\\TH:i:s'),
            // Format with timezone information
            // (offsets will be normalized to the persistence timezone format,
            // UTC for integer fields, LOCALTIME for native DATETIME fields)
            'mydatefield_2' => $myDate->format('c'),
            // Pass \DateTimeInterface objects directly
            'mydatefield_3' => $myDate,
        ],
    ],
]);
Copied!

Impact 

TYPO3 now provides accurate and consistent handling of ISO 8601 dates, eliminating previous issues related to timezone interpretation and LOCALTIME representation.

Feature: #105624 - PSR-14 event after a backend user password has been reset 

See forge#105624

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent has been introduced. It is dispatched right after a backend user has reset their password and it has been hashed and persisted to the database.

The event contains the corresponding backend user UID.

Example 

The corresponding event listener class:

<?php

namespace Vendor\MyPackage\Backend\EventListener;

use TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class PasswordHasBeenResetEventListener
{
    #[AsEventListener('my-package/backend/password-has-been-reset')]
    public function __invoke(PasswordHasBeenResetEvent $event): void
    {
        $userId = $event->userId;
        // Add custom logic for the backend user UID
    }
}
Copied!

Impact 

It is now possible to add custom business logic after a backend user has reset their password using the new PSR-14 event PasswordHasBeenResetEvent .

Feature: #105783 - Notify backend users on failed MFA verification attempts 

See forge#105783

Description 

TYPO3 now notifies backend users by email when a failed MFA (multi-factor authentication) verification attempt occurs. The notification is sent only if an MFA provider is configured and the user has a valid email address in their profile.

Impact 

TYPO3 backend users now benefit from improved security awareness through immediate email notifications about failed MFA verification attempts. This feature is particularly useful in cases where backend accounts with active MFA configuration are targeted by unauthorized access attempts.

Feature: #105833 - Extended page tree filter functionality 

See forge#105833

Description 

The page tree is one of the central components in the TYPO3 backend, particularly for editors. However, in large installations, the page tree can quickly become overwhelming and difficult to navigate. To maintain a clear overview, the page tree can be filtered using basic terms, such as the page title or ID.

To enhance the filtering capabilities, the new PSR-14 event \TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent has been introduced. This event allows developers to extend the filter functionality and process the given search phrase in more advanced ways.

Using this event, it is for example possible to evaluate a given URL or to add additional field matchings, such as filtering pages by their doktype or their configured backend layout.

The event provides the following public properties:

$searchParts:
The search parts to be used for filtering
$searchUids:
The uids to be used for filtering by a special search part, which is added by Core always after listener evaluation
$searchPhrase
The complete search phrase, as entered by the user
$queryBuilder:
The current QueryBuilder instance to provide context and to be used to create search parts

Example 

The following event listener class demonstrates how to add additional conditions to the page tree filter using the PHP attribute #[AsEventListener] for registration.

EXT:my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Connection;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(BeforePageTreeIsFilteredEvent $event): void
    {
        // Add an additional UID to the filter
        $event->searchUids[] = 123;

        // Add evaluation of doktypes to the filter
        if (preg_match('/doktype:([0-9]+)/i', $event->searchPhrase, $match)) {
            $doktype = $match[1];
            $event->searchParts = $event->searchParts->with(
                $event->queryBuilder->expr()->eq(
                    'doktype',
                    $event->queryBuilder->createNamedParameter($doktype, Connection::PARAM_INT)
                )
            );
        }
    }
}
Copied!

Impact 

With the new PSR-14 event BeforePageTreeIsFilteredEvent , custom functionality and advanced evaluations can now be added to enhance the page tree filter.

Feature: #106072 - Introduce regex-based replacements for slugs 

See forge#106072

Description 

A second replacement configuration array has been added to support regular expression (regex)-based definitions. This allows defining case-insensitive or wildcard replacements for slug generation.

Impact 

Slug fields now support a new regexReplacements configuration array inside generatorOptions.

Example TCA configuration
$GLOBALS['TCA'][$table]['columns']['slug']['config']['generatorOptions']['regexReplacements'] => [
    // Case-insensitive replacement of Foo, foo, FOO, etc. with "bar"
    '/foo/i' => 'bar',
    // Remove string wrapped in parentheses
    '/\(.*\)/' => '',
    // Same, using a custom regex delimiter
    '@\(.*\)@' => '',
];
Copied!

Feature: #106074 - Show editor information in workspace "Publish" module 

See forge#106074

Description 

The "Last changed" column in the Content > Publish module has been improved. It now displays the username and avatar of the editor who most recently modified the corresponding record.

The column shows the editor's username in a badge, or Unknown if no editor information is available. This helps reviewers quickly identify who last worked on each workspace record.

Impact 

Workspace information now reveals the last editor of a record within the workspace context.

Feature: #106092 - Associative array keys for TCA valuePicker items 

See forge#106092

Description 

It is now possible to define associative array keys for the items configuration of the TCA type valuePicker. The new keys are called label and value.

This follows the change made previously to the items configuration of the TCA types select, radio, and check. See forge#99739.

Impact 

The TCA items configuration can now be defined in a more consistent and readable way using associative array keys. This eliminates ambiguity about whether the label or value comes first.

Optional keys such as icon, group, or description can be used as needed.

Feature: #106232 - Provide record title tag provider 

See forge#106232

Description 

The class \TYPO3\CMS\Core\PageTitle\RecordTitleProvider introduces a new page title provider with the identifier recordTitle. It is executed before the \SeoTitlePageTitleProvider, which uses the TypoScript identifier seo.

This provider can be used by third-party extensions to set the page title programmatically.

EXT:my_extension/Classes/Controller/ItemController.php
use MyVendor\MyExtension\Domain\Model\Item;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Core\PageTitle\RecordTitleProvider;

final class ItemController extends ActionController
{
    public function __construct(
        private readonly RecordTitleProvider $recordTitleProvider
    ) {
    }

    public function showAction(Item $item): ResponseInterface
    {
        $this->recordTitleProvider->setTitle($item->getTitle());
        $this->view->assign('item', $item);
        return $this->htmlResponse();
    }
}
Copied!

Impact 

A dedicated provider is now available for extensions to set page titles without needing to implement their own custom provider.

Feature: #106363 - PSR-14 event for modifying URLs in redirects:checkintegrity 

See forge#106363

Description 

A new PSR-14 event \TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent has been introduced. It allows extensions to register event listeners to modify the list of URLs processed by the CLI command redirects:checkintegrity.

Example 

The following example shows an event listener that uses the PHP attribute #[AsEventListener] to register itself and adds the URLs found in a site's XML sitemap to the list of URLs checked by the redirects integrity command.

EXT:my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent;

final readonly class MyEventListener
{
    public function __construct(
        private RequestFactory $requestFactory,
    ) {}

    #[AsEventListener]
    public function __invoke(AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent $event): void
    {
        $pageUrls = $event->getPageUrls();

        $additionalOptions = [
            'headers' => ['Cache-Control' => 'no-cache'],
            'allow_redirects' => false,
        ];

        $site = $event->getSite();

        foreach ($site->getLanguages() as $siteLanguage) {
            $sitemapIndexUrl = rtrim((string)$siteLanguage->getBase(), '/') . '/sitemap.xml';
            $response = $this->requestFactory->request(
                $sitemapIndexUrl,
                'GET',
                $additionalOptions,
            );
            $sitemapIndex = simplexml_load_string($response->getBody()->getContents());

            foreach ($sitemapIndex as $sitemap) {
                $sitemapUrl = (string)$sitemap->loc;
                $response = $this->requestFactory->request(
                    $sitemapUrl,
                    'GET',
                    $additionalOptions,
                );
                $sitemap = simplexml_load_string($response->getBody()->getContents());
                foreach ($sitemap as $url) {
                    $pageUrls[] = (string)$url->loc;
                }
            }
        }

        $event->setPageUrls($pageUrls);
    }
}
Copied!

Feature: #106405 - TypolinkBuilderInterface 

See forge#106405

Description 

A new interface \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface has been introduced to provide a more flexible way to generate links in TYPO3.

The interface defines a buildLink() method that replaces the previous build() method approach used when extending from AbstractTypolinkBuilder .

All Core TypolinkBuilder implementations now implement this interface and use dependency injection for improved service composition and testability.

The interface method signature is as follows:

use TYPO3\CMS\Frontend\Typolink;

public function buildLink(
    array $linkDetails,
    array $configuration,
    ServerRequestInterface $request,
    string $linkText = ''
): LinkResultInterface;
Copied!

Impact 

  • All implementations of TypolinkBuilderInterface are automatically configured as public services in the dependency injection container, removing the need for manual service configuration.
  • TypolinkBuilder classes can now use proper dependency injection through their constructors, improving testability and aligning with TYPO3's architectural best practices.
  • The \ServerRequestInterface is now passed directly, providing access to the request context without relying on global state.
  • The new interface introduces a cleaner separation of concerns and more explicit parameter passing.

Example usage 

Creating a custom TypolinkBuilder using the new interface:

EXT:my_extension/Classes/Typolink/MyCustomLinkBuilder.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\LinkResult;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

final readonly class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function __construct(
        private MyCustomService $customService,
        private AnotherService $anotherService,
    ) {}

    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = ''
    ): LinkResultInterface {
        // Access ContentObjectRenderer from the request
        $contentObjectRenderer = $request->getAttribute('currentContentObject');

        // Use injected services
        $processedData = $this->customService->process($linkDetails);

        // Build and return link result
        return new LinkResult($processedData['url'], $linkText);
    }
}
Copied!

Registering the TypolinkBuilder class is still necessary via $GLOBALS['TYPO3_CONF_VARS'] .

Feature: #106415 - Add stdWrap to config.htmlTag.attributes.[attr] 

See forge#106415

Description 

Each attribute within the TypoScript option config.htmlTag.attributes.[attr] now supports all stdWrap properties.

This option controls the attributes of the single <html> element of a rendered page.

Impact 

It is now possible to use userFunc, override, or getData within TypoScript:

Using override in TypoScript
config.htmlTag.attributes{
    my-attribute = 123
    my-attribute.override = 456
}
Copied!
Using userFunc in TypoScript
config.htmlTag.attributes {
    my-attribute = 123
    my-attribute.userFunc = MyVendor\\MyExtension\\HtmlTagEnhancer->overrideMyAttribute
}
Copied!

Feature: #106477 - Allow YAML imports and placeholder processing when creating new forms 

See forge#106477

Description 

The FormManagerController now uses the YamlFileLoader when creating new forms from templates. This change enables the processing of placeholders within template files, such as environment variables in the format %env(ENV_NAME)%, as well as the import of other YAML files.

This enhancement allows for more flexible form templates that can adapt to different environments through environment variable substitution and YAML imports.

Examples 

EXT:my_extension/Resources/Private/Backend/Templates/FormEditor/Yaml/NewForms/ContactForm.yaml
imports:
  - { resource: 'User.yaml' }
identifier: contactForm
label: 'Contact Form %env(ENV_NAME)%'
type: Form
renderables:
  -
    type: Page
    identifier: message
    label: Message
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: text-1
        label: Subject
Copied!
EXT:my_extension/Resources/Private/Backend/Templates/FormEditor/Yaml/NewForms/User.yaml
renderables:
  -
    type: Page
    identifier: user
    label: User data
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: username
        label: Username
Copied!

Impact 

Form templates can now contain environment variable placeholders using the %env(ENV_NAME)% syntax and import other YAML files. These placeholders and imports are automatically resolved when new forms are created from the template.

Feature: #106510 - Add PSR-14 events to Extbase Backend::getObjectCountByQuery method 

See forge#106510

Description 

The class \TYPO3\CMS\Extbase\Persistence\Generic\Backend is the central entity for retrieving data from the database within the Extbase persistence framework.

Since 2013, the getObjectDataByQuery() method has supported events (previously signals) to allow modification of data retrieval.

In many use cases, especially when used together with \QueryResult, another key method is involved: getObjectCountByQuery(). This method is frequently used in combination with Fluid templates.

Until now, extensions or other code using the existing events for data retrieval could not ensure consistent modification of queries between data retrieval and counting operations, resulting in mismatched query results.

The getObjectCountByQuery() method has now been enhanced with new PSR-14 events, enabling extensions to modify all aspects of query processing within Extbase's generic Backend to achieve consistent results.

The new events are:

  • ModifyQueryBeforeFetchingObjectCountEvent
    Allows modification of the query before it is passed to the storage backend.
  • ModifyResultAfterFetchingObjectCountEvent
    Allows adjustment of the result after the query has been executed.

Typically, an extension should implement these events pairwise:

  • ModifyQueryBeforeFetchingObjectCountEvent together with ModifyQueryBeforeFetchingObjectDataEvent
  • ModifyResultAfterFetchingObjectCountEvent together with ModifyResultAfterFetchingObjectDataEvent

Feature: #106637 - Implement accessible combobox pattern 

See forge#106637

Description 

A new ARIA 1.2–compliant combobox web component has been introduced, replacing the legacy value picker select pattern. The implementation follows the W3C accessibility guidelines and provides complete keyboard navigation support.

FormEngine elements, including EmailElement , InputTextElement , and NumberElement , have been updated to use the new combobox component instead of the previous value picker implementation. The link browser components have also been adapted to use the combobox pattern.

Impact 

The new combobox component offers full keyboard navigation using the arrow keys, Enter, Tab, and Escape.

It includes visual selection indicators with checkmarks and a clear button for resetting the input value, improving accessibility and overall usability in the TYPO3 backend.

Feature: #106686 - Enhance file browsers with column selector 

See forge#106686

Description 

The file browsers used in the file selector (for example, in file fields) and the file link browser (for example, for RTE links) have been enhanced to support the column selector component.

This allows backend users to customize which columns are displayed (for example, "alternative", "timestamp", and so on), improving usability and aligning the interface with the standard File List module.

This enhancement is particularly useful in large file storages, making it easier to locate recently updated files or identify large files more quickly.

Impact 

The file selector and link browser interfaces now include a column selector. This improves consistency across TYPO3 backend modules and provides users with greater control over the visibility of file metadata.

No additional configuration is required for this feature.

Whether the column selector is shown is still determined by the user TSconfig option options.file_list.displayColumnSelector.

Feature: #106739 - Scheduler tasks as native TCA table 

See forge#106739

Description 

For historical reasons, the TYPO3 system extension typo3/cms-scheduler used a special mechanism for persisting data to the database. This was achieved by serializing the entire task object into a single field of the database table tx_scheduler_task.

In TYPO3 v14, this behavior has been reworked. The logic for custom fields has been split into a dedicated database field parameters of type json and a new database field tasktype that stores the scheduler task name or CLI command.

An upgrade wizard automatically migrates all existing scheduler tasks to the new database structure.

Impact 

With this change, TCA is now defined for the tx_scheduler_task table in TYPO3. This provides several advantages over the previous implementation:

  • The editing interface is now handled via FormEngine, making it more flexible and extensible.
  • Changes are stored to the database via DataHandler, which allows customization of persistence operations. The history and audit functionality is now also available for scheduler tasks.
  • Database entries of tx_scheduler_task can now be exported and imported using the standard import/export functionality.
  • Deleted tasks can be restored via the recycler module.

Additional functionality such as support for automated database restrictions and the TCA schema is available as well.

Feature: #106752 - Add password hashing option to SaveToDatabase finisher 

See forge#106752

Description 

A new option hashed has been added to the \SaveToDatabaseFinisher of the system extension typo3/cms-form .

When saving form data to a database table, setting hashed: true for a field causes the value to be hashed using the default frontend password hashing mechanism before it is written to the database.

This improves security by preventing passwords from being stored in plain text.

Example usage in a form definition:

EXT:my_extension/Configuration/Form/ExampleForm.form.yaml
- identifier: SaveToDatabase
  options:
    table: 'fe_users'
    elements:
      password:
        mapOnDatabaseColumn: 'password'
        hashed: true
Copied!

Impact 

Integrators can now ensure secure password storage when saving form data with the \SaveToDatabaseFinisher, without implementing custom logic.

Feature: #106839 - Introduce shell auto-completion for the typo3 command 

See forge#106839

Description 

A new CLI command vendor/bin/typo3 completion has been added to the typo3 CLI dispatcher script.

This command enables shell auto-completion for supported shells, allowing developers to use the Tab key to trigger command and option suggestions.

The completion command is provided by the symfony/console package and is not a custom implementation.

This ensures compatibility with ongoing improvements in the Symfony ecosystem and benefits from a broad user base and community support.

Supported shells 

The command reports unsupported shells and lists available ones:

# bin/typo3 completion shell
Detected shell "shell", which is not supported by Symfony shell completion
(supported shells: "bash", "fish", "zsh").
Copied!

Installation modes 

The command supports two installation modes — static and dynamic. Run vendor/bin/typo3 completion --help to see detailed usage instructions and the supported shells (bash, fish, zsh).

Static installation 

Dump the completion script to a file and source it manually or install it globally, for example:

vendor/bin/typo3 completion bash | sudo tee /etc/bash_completion.d/typo3
Copied!

Or dump the script to a local file and source it:

bin/typo3 completion bash > completion.sh
source completion.sh
Copied!

To make it permanent, add the following line to your " /.bashrc" file:

 /.bashrc
source /path/to/completion.sh
Copied!

Dynamic installation 

Add an eval line to your shell configuration file (for example ~/.bashrc):

eval "$(/var/www/html/vendor/bin/typo3 completion bash)"
Copied!

Impact 

The typo3 CLI dispatcher now supports shell auto-completion, improving the user experience without affecting existing command usage. This also lays the foundation for further enhancements such as improved auto-completion for command options and arguments.

The following existing commands already provide completion for their arguments:

  • redirects:cleanup
  • redirects:checkintegrity

Example 

The following example shows how to add auto-completion support to a custom Symfony console command:

EXT:my_extension/Classes/Command/GreetCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Input\InputArgument;

#[AsCommand(
    name: 'myextension:greet',
)]
class GreetCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->addArgument(
                'names',
                InputArgument::IS_ARRAY,
                'Who do you want to greet (separate multiple names with a space)?',
                null,
                function (CompletionInput $input): array {
                    // Value already typed by the user, e.g. "myextension:greet Fa"
                    // before pressing Tab — this will contain "Fa"
                    $currentValue = $input->getCompletionValue();

                    // Example of available usernames
                    $availableUsernames = ['jane', 'jon'];

                    return $availableUsernames;
                }
            );
    }
}
Copied!

For more details, see Symfony Console Adding Argument Option Value Completion and also for testing purpose see Testing the Completion script.

Feature: #106934 - Add recently used records to record wizards 

See forge#106934

Description 

A new dynamic category "Recently used" has been added to the record creation wizards for the following components:

  • Content elements
  • Form elements
  • Dashboard widgets

This category lists the record or form types that a user has recently selected in the respective wizard. It remains hidden until at least one record has been created, keeping the interface uncluttered for new users.

This enhancement allows backend users to quickly access and reuse frequently used record types, improving workflow efficiency and usability.

Impact 

All record wizards now feature a "Recently used" section. This feature is enabled by default and requires no additional configuration. Users can manage the display of this category in their personal backend settings.

This improvement streamlines access to frequently used records, enhancing the overall editing experience for editors and integrators.

Example 

When creating a new content element, users will now see a "Recently used" section listing previously used content types, making them accessible with a single click.

Feature: #106945 - Allow usage of Symfony validators in Extbase 

See forge#106945

Description 

Extbase models and controllers now support the use of Symfony Validators. Validators are based on Symfony Constraints, which can be added as attributes to domain model properties and controller methods.

Once a constraint attribute is detected while reflecting properties or methods, it is decorated by the new ConstraintDecoratingValidator class, which is compatible with Extbase's \ValidatorInterface.

Decorated constraints may include localizable messages. If a message contains valid LLL: syntax, the label will be translated automatically. The decorating validator also handles message parameters by converting named parameters such as {{ value }} into sprintf-compatible placeholders like %1$s.

Example 

EXT:my_extension/Classes/Domain/Model/MyModel.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use Symfony\Component\Validator\Constraints as Assert;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Assert\WordCount(max: 200, maxMessage: 'Biography must not exceed 200 words.')]
    protected string $biography = '';

    #[Assert\CssColor(message: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validator.avatarColor.error')]
    protected string $avatarColor = '';

    #[Assert\Iban]
    protected string $iban = '';

    public function getBiography(): string
    {
        return $this->biography;
    }

    public function setBiography(string $biography): void
    {
        $this->biography = $biography;
    }

    public function getAvatarColor(): string
    {
        return $this->avatarColor;
    }

    public function setAvatarColor(string $avatarColor): void
    {
        $this->avatarColor = $avatarColor;
    }

    public function getIban(): string
    {
        return $this->iban;
    }

    public function setIban(string $iban): void
    {
        $this->iban = $iban;
    }
}
Copied!

Impact 

A wide range of Symfony validators can now be used directly in Extbase. This provides a more flexible and standardized validation workflow without the need to implement custom validators, as Symfony already ships with a large number of predefined constraints.

Feature: #106972 - Configure searchable fields 

See forge#106972

Description 

TYPO3 now automatically includes all fields of suitable types in backend search operations, e.g., in the List module.

This eliminates the need for the previously used TCA ctrl option searchFields, which has been removed.

Instead, a new per-field configuration option searchable has been introduced. It allows integrators to fine-tune whether a specific field should be included in backend search queries.

By default, all fields of supported types are considered searchable. To exclude a field from being searchable, set the following in the field’s TCA configuration:

'my_field' => [
    'config' => [
        'type' => 'input',
        'searchable' => false,
    ],
],
Copied!

Note that until searchFields is manually removed from your TCA, the automatic TCA migration sets all suitable fields, which are not included in the searchFields configuration, to searchable => false to keep current behavior.

Supported Field Types 

The following TCA field types support the searchable option and are automatically considered in searches unless explicitly excluded:

  • color
  • datetime (when not using a custom dbType)
  • email
  • flex
  • input
  • json
  • link
  • slug
  • text
  • uuid

Unsupported field types such as file, inline, password or group are excluded from search and do not support the searchable option.

Impact 

  • Backend search becomes more consistent and automatic.
  • No need to manually maintain a searchFields list in TCA.
  • Integrators have more granular control over search behavior on a field level.
  • Custom fields can easily be excluded from search using the searchable option.

Migration 

If your extension previously relied on the searchFields TCA option, remove it from the ctrl section and instead define 'searchable' => false on fields that should be excluded from search results.

No action is needed if the default behavior (search all suitable fields) is acceptable.

Example 

return [
    'columns' => [
        'title' => [
            'config' => [
                'type' => 'input',
                'searchable' => true, // optional, true by default
            ],
        ],
        'notes' => [
            'config' => [
                'type' => 'text',
                'searchable' => false, // explicitly excluded
            ],
        ],
    ],
];
Copied!

Feature: #106992 - Remember last opened category in record wizards 

See forge#106992

Description 

Following forge#106934, which introduced the dynamic Recently used category in record wizards, the component has been extended to store the last selected category. When opening a record wizard, for example, the wizard to create new content elements, it will now automatically preselect the category that was last used.

This enhancement improves usability and consistency, especially in installations with many categories, including those added by third-party extensions.

Impact 

Record wizards now automatically preselect the last used category when opened again.

No migration or configuration is required. The feature is enabled by default.

Feature: #107036 - Configurable dashboard widgets 

See forge#107036

Description 

Dashboard widgets can now be configured on a per-instance level using the Settings API. This allows widget authors to define settings that editors can modify directly from the dashboard interface, making widgets more flexible and adaptable to different use cases.

Typical configuration options include URLs for RSS feeds, limits on displayed items, or categories for filtering content.

Each widget instance maintains its own configuration, enabling multiple instances of the same widget type with different settings on the same or different dashboards.

Impact 

For editors

  • Dashboard widgets now display a settings (cog) icon when configuration is supported.
  • Clicking the icon opens a modal dialog with configurable options.
  • Settings are applied immediately after saving, and the widget refreshes automatically.
  • Each widget instance is configured independently per user.

For integrators

  • Dashboard widgets can now expose user-configurable options.
  • Settings are validated using the existing Settings API type system.
  • Widget instances maintain independent configurations, allowing flexible layouts.
  • No extra configuration is required—configurable widgets automatically show the settings icon.

For widget authors

  • Widgets can migrate from WidgetInterface to WidgetRendererInterface to define settings and use configured values during rendering.
  • Settings are validated and processed automatically via the Settings API.
  • The widget context provides a Settings object containing the current configuration.
  • All existing Settings API types are supported (string, int, bool, url, etc.).

Currently configurable widgets 

The following core widgets now support configuration:

RSS widget
  • Label – custom title for the widget instance
  • Feed URL – RSS feed URL to display (supports URL validation)
  • Limit – number of RSS items to show (default: 5)
  • Lifetime – cache duration in seconds for the RSS feed
Pages with internal note widget
  • Category – filter notes by category (All, Default, Instructions, Template, Notes, Todo)
  • Includes an upgrade wizard to migrate existing widgets of this type to the new format.

Example 

Widget authors can implement configurable widgets by migrating from the current interface WidgetInterface to the new renderer interface WidgetRendererInterface , which allows defining settings in the widget renderer:

EXT:my_extension/Classes/Widgets/ConfigurableWidget.php
<?php

use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetResult;

class ConfigurableWidget implements WidgetRendererInterface
{
    public function getSettingsDefinitions(): array
    {
        return [
            new SettingDefinition(
                key: 'title',
                type: 'string',
                default: 'Default Title',
                label: 'my_extension.my_widget:settings.label',
                description: 'my_extension.my_widget:settings.description.label',
            ),
            new SettingDefinition(
                key: 'limit',
                type: 'int',
                default: 10,
                label: 'my_extension.my_widget:settings.limit',
                description: 'my_extension.my_widget:settings.description.limit',
            ),
        ];
    }

    public function renderWidget(WidgetContext $context): WidgetResult
    {
        $settings = $context->settings;
        $title = $settings->get('title');
        $limit = $settings->get('limit');

        // Use settings to customize widget output
        return new WidgetResult(
            label: $title,
            content: '<!-- widget content -->',
            refreshable: true
        );
    }
}
Copied!

Editors can configure these widgets directly in the dashboard interface:

  1. Navigate to the dashboard containing the widget.
  2. Click the settings (cog) icon on the widget.
  3. Modify the available settings in the modal dialog.
  4. Click Save to apply the changes.

The widget automatically refreshes with the updated configuration.

Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support 

See forge#107047

FlexForm direct plugin registration 

The methods \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() and \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin() have been extended to accept a FlexForm definition directly via an additional $flexForm argument.

This new argument allows extensions to provide the FlexForm data structure when registering a plugin. The FlexForm can either be a reference to a FlexForm XML file (for example, FILE:EXT:my_extension/Configuration/FlexForm.xml) or the XML content itself.

This simplifies configuration and avoids the need to define the FlexForm separately in TCA.

Examples 

Direct FlexForm plugin registration

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'My Plugin Title',
    'my-extension-icon',
    'plugins',
    'Plugin description',
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Alternatively, using addPlugin() when not using Extbase:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
ExtensionManagementUtility::addPlugin(
    [
        'My Plugin Title',
        'my_plugin',
        'my-extension-icon'
    ],
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Internally, this adds the FlexForm definition to the ds option of the plugin via the columnsOverrides configuration and also adds the pi_flexform field to the showitem list. For more information, see Breaking: #107047 - Remove pointer field functionality of TCA flex, which describes the migration of the ds option from multi-entry to single-entry.

FlexFormTools schema parameter requirement 

The service FlexFormTools has been refactored to remove its dependency on $GLOBALS['TCA'] , which caused architectural issues.

The following methods now support an explicit $schema parameter that accepts either a TcaSchema object or a raw TCA configuration array:

  • getDataStructureIdentifier()
  • parseDataStructureByIdentifier()
  • cleanFlexFormXML()

Previously, these methods had no schema parameter and relied on $GLOBALS['TCA'] internally, which was problematic during schema building.

Calling code must now explicitly provide schema data, either as:

  • A resolved TcaSchema object (for normal usage)
  • A raw TCA configuration array (for schema building contexts)

This architectural improvement eliminates circular dependencies and allows FlexFormTools to be used during schema building processes where TCA Schema objects are not yet available, resolving issues in components such as the RelationMapBuilder .

FlexFormTools with TCA Schema

Example using TCA Schema
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Using TCA Schema object
$tcaSchema = $tcaSchemaFactory->get('tt_content');
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $tcaSchema
);
Copied!

FlexFormTools with raw TCA array

Example using raw TCA configuration
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Using raw TCA configuration array
$rawTca = $fullTca['tt_content'];
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $rawTca
);
Copied!

Schema building context

Example usage in RelationMapBuilder
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// In RelationMapBuilder - previously not possible
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

foreach ($tca as $table => $tableConfig) {
    foreach ($tableConfig['columns'] ?? [] as $fieldName => $fieldConfig) {
        if ($fieldConfig['config']['type'] === 'flex') {
            // Can now use raw TCA during schema building
            $dataStructure = $flexFormTools->parseDataStructureByIdentifier(
                $identifier,
                $tableConfig // Raw TCA array
            );
        }
    }
}
Copied!

Impact 

Direct FlexForm plugin registration

This enhancement simplifies plugin configuration and FlexForm integration, as FlexForms can now be registered directly with the plugin. The call ExtensionManagementUtility::addPiFlexFormValue() is no longer required. This method has been deprecated; see Deprecation: #107047 - ExtensionManagementUtility::addPiFlexFormValue().

FlexFormTools schema support

The service now automatically detects the input type and uses the appropriate resolution strategy for both TCA Schema objects and raw TCA arrays. It no longer relies on $GLOBALS['TCA'] , allowing direct control over the service and making it usable during schema building where no TCA Schema is available.

Technical details

The service uses PHP union types ( array|TcaSchema) and automatically routes to the appropriate internal methods. Both input types produce identical normalized output, ensuring consistent data structures for all FlexFormTools consumers.

Feature: #107056 - Introduce headerData and footerData ViewHelpers 

See forge#107056

Description 

Two new Fluid ViewHelpers have been introduced to allow injecting arbitrary content into the HTML <head> or before the closing </body> tag of a rendered page:

  • <f:page.headerData> - injects content into the <head> section
  • <f:page.footerData> - injects content before the closing </body> tag

The ViewHelpers internally use the PageRenderer API and are useful when existing ViewHelpers such as <f:asset.css> or <f:asset.script> do not support all required attributes or use cases (for example, dns-prefetch, preconnect, tracking scripts, or inline JavaScript).

Example usage for <f:page.headerData>:

<f:page.headerData>
    <link
        rel="preload"
        href="/fonts/myfont.woff2"
        as="font"
        type="font/woff2"
        crossorigin="anonymous"
    >
    <link rel="dns-prefetch" href="//example-cdn.com">
    <link rel="preconnect" href="https://example-cdn.com">
</f:page.headerData>
Copied!

Example usage for <f:page.footerData>:

<f:page.footerData>
    <script>
        var _paq = window._paq = window._paq || [];
        _paq.push(['trackPageView']);
        _paq.push(['enableLinkTracking']);
        (function() {
            var u = "https://your-matomo-domain.example.com/";
            _paq.push(['setTrackerUrl', u + 'matomo.php']);
            _paq.push(['setSiteId', '1']);
            var d = document,
                g = d.createElement('script'),
                s = d.getElementsByTagName('script')[0];
            g.async = true;
            g.src = u + 'matomo.js';
            s.parentNode.insertBefore(g, s);
        })();
    </script>
</f:page.footerData>
Copied!

Both ViewHelpers output the given content as-is. Any user-supplied input passed to these ViewHelpers must be escaped manually to prevent Cross-Site Scripting (XSS) vulnerabilities.

Impact 

Extension authors and integrators can now use the new ViewHelpers to add raw HTML content, such as <link> or <script> tags, directly into the rendered page output.

Feature: #107104 - Introduce UrlFactory JavaScript module 

See forge#107104

Description 

TYPO3 already uses the native URL and URLSearchParams objects when working with URLs. The module @typo3/core/factory/url-factory.js has been introduced to provide a consistent and convenient way to create and manage these objects.

Impact 

URL and URLSearchParams objects can now be created by the factory's createUrl() and createSearchParams() methods.

The method createUrl() creates a full URL object and automatically sets its base. It accepts the following arguments:

  • url - string
  • parameters - mixed

If provided, the parameters value is passed to createSearchParams() (described below).

The method createSearchParams() creates a URLSearchParams object and accepts the following argument:

  • parameters - mixed

Parameters can be passed as plain string values or nested objects. Passing a plain array is not supported.

Examples 

The following examples assume the existence of TYPO3.settings.ajaxUrls.my_dedicated_endpoint, pointing to the route /custom_endpoint, while being on https://localhost for documentation purposes.

Create a URL object
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const url = UrlFactory.createUrl(
    TYPO3.settings.ajaxUrls.my_dedicated_endpoint
);
console.log(url.toString());
// https://localhost/custom_endpoint
Copied!
Create a URL object containing a query string from a nested object
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const url = UrlFactory.createUrl(
    TYPO3.settings.ajaxUrls.my_dedicated_endpoint,
    {
        foo: 'bar',
        baz: {
            hello: 'world',
        },
    }
);
console.log(url.toString());
// https://localhost/custom_endpoint?foo=bar&baz[hello]=world
Copied!
Create a URLSearchParams object from a string input
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const urlSearchParams = UrlFactory.createSearchParams(
    'foo=bar&baz=bencer'
);
console.log(urlSearchParams.toString());
// foo=bar&baz=bencer
Copied!
Create a URLSearchParams object from an object input
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const urlSearchParams = UrlFactory.createSearchParams({
    foo: 'bar',
    baz: 'bencer',
});
console.log(urlSearchParams.toString());
// foo=bar&baz=bencer
Copied!

Feature: #107105 - Introduce expression for accessing site locale 

See forge#107105

Description 

A new Symfony ExpressionLanguage expression locale() has been introduced.

This expression allows integrators and developers to directly access the current site locale, which is provided as a locale object. All public methods of this object are available for use.

For more information, refer to the API documentation: API

Example 

Using locale() in TypoScript conditions
[locale().getName() == "en-US"]
    page.20.value = bar
[END]
[locale().getCountryCode() == "US"]
    page.30.value = foo
[END]
[locale().isRightToLeftLanguageDirection()]
    page.40.value = bar
[END]
Copied!
Using locale() in a form variant definition
variants:
  - identifier: language-variant-1
    condition: 'locale().getName() == "en-US"'
    label: 'First name'
Copied!

Impact 

Developers can now compare or evaluate the site locale directly in expressions, without using siteLanguage("locale").

Feature: #107151 - Add AsNonSchedulableCommand attribute for CLI commands 

See forge#107151

Description 

With forge#101567 the usage of Symfony's #[AsCommand] attribute has been introduced, which allows configuring a Symfony CLI command with a corresponding name, description and further options.

It however lacked TYPO3's custom implementation of the schedulable option, which allows flagging a CLI command to be not allowed to be scheduled via the Administration > Scheduler backend module.

This previously required tagging such a command with the schedulable: false tag attribute in the Services.yaml or Services.php definition.

For this, the PHP attribute AsNonSchedulableCommand has been introduced. Any Symfony Command can use this empty attribute. The automatic Scheduler registry will ignore any command with this tag.

By default, a Symfony Command remains schedulable using the regular Symfony attribute. To prevent redundancy, the new attribute #[AsNonSchedulableCommand] should be used only on top of that.

Another advantage is that an IDE like PhpStorm is capable of showing all usages of that attribute inside a project.

Impact 

Developers can now fully embrace using the Symfony #[AsCommand] attribute and still be able to declare a non-schedulable execution within the scope of the same class, without any service registration.

This is achieved by using the #[AsNonSchedulableCommand] in addition to the #[AsCommand] attribute.

Example 

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand('myextension:import', 'Import data from external source')]
#[AsNonSchedulableCommand]
final class ImportCommand extends Command
{
    // ...
}
Copied!

Feature: #107180 - PSR-14 events for Backend Users module 

See forge#107180

Description 

Several PSR-14 events have been added to allow customizations of the Backend Users module - in particular, which users, groups, and file mounts can be viewed in the module.

AfterBackendUserListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend user repository fetches a list of filtered backend users (itself called when displaying the list of users in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Model\Demand containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendUserListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(
        AfterBackendUserListConstraintsAssembledFromDemandEvent $event
    ): void {
        $event->constraints[] = $event->query->eq('admin', 1);
    }
}
Copied!

AfterBackendGroupListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend group repository fetches a list of filtered backend groups (itself called when displaying the list of groups in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Dto\BackendUserGroup containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupListConstraintsAssembledFromDemandEvent $event): void
    {
        $event->constraints[] = $event->query->eq('workspace_perms', 1);
    }
}
Copied!

AfterBackendGroupFilterListIsAssembledEvent 

A list of user groups can be used to filter the users list in the backend module. This event is dispatched right after this list is assembled and makes it possible to modify it.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $backendGroups: An array of backend groups.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupFilterListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupFilterListIsAssembledEvent $event): void
    {
        array_pop($event->backendGroups);
    }
}
Copied!

AfterFilemountsListIsAssembledEvent 

This event is dispatched when the file mounts list is fetched to display in the backend module. It makes it possible to modify this list.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $filemounts: An array of file mounts.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;;

use TYPO3\CMS\Beuser\Event\AfterFilemountsListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterFilemountsListIsAssembledEvent $event): void
    {
        array_pop($event->filemounts);
    }
}
Copied!

Impact 

These events can be used to implement custom user or permission management processes in the Backend Users module. Be aware that this area is security sensitive. Ensure that no unauthorized data exposure or privilege escalation occurs when modifying these queries or lists.

Feature: #107201 - Extended RSS Widget with Atom Support 

See forge#107201

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing RSS widget has been extended to support Atom feeds. This provides a unified solution for both RSS and Atom feed formats within the TYPO3 Dashboard system.

The configurable widget architecture makes it possible to create feed-based widgets that users can configure directly through the dashboard interface. This capability now extends seamlessly to Atom feeds, which are commonly used by platforms such as GitHub for release feeds and project updates.

The RSS widget now automatically detects and parses both RSS and Atom feed formats. Atom feeds are parsed according to the Atom Syndication Format (RFC 4287). This provides comprehensive support for modern feed formats used by many development platforms and services, all within a single widget implementation.

Impact 

  • The existing "RSS Widget" now supports both RSS and Atom feeds automatically.
  • Atom feeds (commonly used by GitHub, GitLab, and other platforms) can now be displayed using the same RSS widget.
  • Widget instances are configurable with custom labels, feed URLs, and display limits.
  • Each widget can be configured independently, allowing multiple feeds of different formats on the same dashboard.
  • Automatic caching ensures optimal performance with configurable cache lifetimes.

Currently configurable options 

The RSS widget supports the following configuration options for both RSS and Atom feeds:

Label
  • Custom title for the widget instance.
  • Optional field that defaults to the widget’s default title.
Feed URL
  • RSS or Atom feed URL to display.
  • Format detection is automatic based on feed content.
  • Can be preconfigured in the service definition to create read-only instances.
Limit
  • Number of feed entries to show (default: 5).
  • Configurable integer value to control widget content density.
Lifetime
  • Cache duration in seconds for the feed (default: 43200 = 12 hours).
  • Advanced setting typically configured by integrators.
  • Not configurable in the user interface.

Example for RSS widget with Atom feed 

Service configuration

EXT:my_extension/Configuration/Services.yaml
services:

  # Button provider for external link
  dashboard.buttons.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
    arguments:
      $title: 'View all releases'
      $link: 'https://github.com/TYPO3/typo3/releases'
      $target: '_blank'

  # RSS widget with Atom feed URL
  dashboard.widget.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
    arguments:
      $buttonProvider: '@dashboard.buttons.github_releases'
      $options:
        feedUrl: 'https://github.com/TYPO3/typo3/releases.atom'
        lifeTime: 43200
        limit: 10
    tags:
      - name: dashboard.widget
        identifier: 'github_releases'
        groupNames: 'general'
        title: 'my_extension.widgets:github_releases.title'
        description: 'my_extension.widgets:github_releases.description'
        iconIdentifier: 'content-widget-rss'
        height: 'large'
        width: 'medium'
Copied!

Usage in the dashboard

  1. Navigate to the dashboard where you want to add the widget.
  2. Click Add widget and select the RSS widget.
  3. Click the settings (cog) icon to customize the widget.
  4. Configure the feed URL (RSS or Atom), limit, and label as needed.
  5. Save the configuration to apply the changes.

The widget automatically detects the feed format and displays entries with titles, publication dates, content summaries, and author information when available.

Feed format support 

The RSS widget now supports both feed formats:

RSS feeds
Item titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Descriptions
displayed as entry content (HTML tags stripped).
Atom feeds
Entry titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Content/Summary
displayed as entry description (HTML tags stripped).
Author information
name, email, and URL when provided in the feed.

Feature: #107240 - Add warning when pasting password with whitespace 

See forge#107240

Description 

A new warning mechanism has been introduced in the backend login form to help users avoid authentication issues when pasting passwords that contain leading or trailing whitespace.

When a password is pasted into the backend login form, TYPO3 now detects if the pasted text contains leading or trailing whitespace characters (spaces, tabs, newlines, etc.) and displays a warning message to the user.

The warning includes an action button that allows users to automatically remove the surrounding whitespace from the pasted password, ensuring successful login attempts.

This feature helps prevent common login failures caused by accidentally copying whitespace along with passwords from password managers, text editors, or other sources.

The implementation includes:

  • Detection of leading and trailing whitespace in pasted passwords
  • Visual warning message displayed to the user
  • One-click action to remove the surrounding whitespace

Example 

The whitespace detection covers various whitespace characters including:

  • Regular spaces (U+0020)
  • Tabs (U+0009)
  • Line breaks (U+000A, U+000D)
  • Other Unicode whitespace characters

Impact 

Users now receive immediate feedback when pasting passwords that contain surrounding whitespace, reducing login failures and improving the overall user experience when authenticating to the TYPO3 backend.

Feature: #107256 - PSR-14 event to modify options in EmailFinisher 

See forge#107256

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent has been introduced. This event is dispatched before the \EmailFinisher is initialized and allows listeners to modify the finisher options dynamically.

This enables developers to customize email behavior programmatically, such as:

  • Setting alternative recipients based on frontend user permissions
  • Modifying the email subject or content dynamically
  • Replacing recipients with developer email addresses in test environments
  • Adding or removing CC or BCC recipients conditionally
  • Customizing reply-to addresses

The event provides access to both the finisher context (read-only) and the options array, allowing for flexible manipulation of the email configuration.

To modify the \EmailFinisher options, the following methods are available:

  • getFinisherContext(): Returns the FinisherContext containing form runtime and request information
  • getOptions(): Returns the current finisher options array
  • setOptions(): Allows setting the modified options array

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent;

final class BeforeEmailFinisherInitializedEventListener
{
    #[AsEventListener('my_extension/form/modify-email-finisher-options')]
    public function __invoke(BeforeEmailFinisherInitializedEvent $event): void
    {
        $options = $event->getOptions();
        $context = $event->getFinisherContext();

        // Overwrite recipients based on FormContext
        if ($context->getFormRuntime()->getFormDefinition()->getIdentifier() === 'my-form-123') {
            $options['recipients'] = ['user@example.org' => 'John Doe'];
        }

        // Modify subject dynamically
        $options['subject'] = 'Custom subject: ' . ($options['subject'] ?? '');

        // Clear CC and BCC recipients
        $options['replyToRecipients'] = [];
        $options['blindCarbonCopyRecipients'] = [];

        $event->setOptions($options);
    }
}
Copied!

Impact 

It is now possible to dynamically modify \EmailFinisher options before email processing begins, using the new PSR-14 event BeforeEmailFinisherInitializedEvent . This provides developers with full control over email configuration without needing to extend or override the \EmailFinisher class.

Feature: #107281 - Type-specific TCAdefaults support 

See forge#107281

Description 

The TCAdefaults configuration has been extended to support type-specific syntax similar to TCEFORM, enabling different default values based on the record type.

This allows configuration like:

Page TSconfig
# Field-level default (applies to all content types)
TCAdefaults.tt_content.header_layout = 1

# Type-specific defaults (applies only to specific content types)
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.frame_class.types.textmedia = ruler-before
Copied!

The same syntax is supported in User TSconfig as well.

Type-specific defaults take precedence over field-level defaults, and Page TSconfig overrides User TSconfig following the established inheritance pattern.

Implementation details 

The feature is implemented in two main areas:

  • Backend forms: The DatabaseRowInitializeNew class now processes type-specific defaults when creating new records in the backend.
  • DataHandler: The DataHandler class supports type-specific defaults when creating records programmatically via the PHP API.

Fallback behavior 

If no type-specific default is found for a given record type, the system falls back to:

  1. Field-level TCAdefaults configuration
  2. TCA 'default' configuration
  3. Database field default value

Automatic field discovery 

This enhancement makes TCAdefaults consistent with TCEFORM patterns and enables automatic field discovery.

Examples 

User TSconfig - Basic type-specific defaults
TCAdefaults.tt_content.header_layout = 1
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.header_layout.types.image = 2
Copied!
Page TSconfig - Multiple fields with type-specific overrides
TCAdefaults.tt_content {
    header_layout = 1
    header_layout.types.textmedia = 3
    header_layout.types.image = 2

    frame_class = default
    frame_class.types.textmedia = ruler-before
    frame_class.types.image = none

    space_before_class = none
}
Copied!
PHP API usage - DataHandler with type-specific defaults
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$datamap = [
    'tt_content' => [
        'NEW123' => [
            'pid' => 42,
            'CType' => 'textmedia',
            'header' => 'My Content Element',
            // header_layout and frame_class will be set automatically
            // based on type-specific TCAdefaults configuration
        ],
    ],
];
$dataHandler->start($datamap, [], $backendUser);
$dataHandler->process_datamap();
Copied!

Impact 

This feature provides a more flexible and consistent way to configure default values for different record types, reducing repetitive configuration and improving the user experience when creating new records.

The type-specific syntax aligns TCAdefaults with the established TCEFORM pattern, making the configuration more intuitive for TYPO3 developers and integrators.

Feature: #107322 - New PSR-14 AfterRichtextConfigurationPreparedEvent 

See forge#107322

Description 

A new PSR-14 event AfterRichtextConfigurationPreparedEvent has been added.

To modify the configuration, the following methods are available:

  • setConfiguration()
  • getConfiguration()

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Configuration\Event\AfterRichtextConfigurationPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class AfterRichtextConfigurationPreparedEventListener
{
    #[AsEventListener('my_extension/configuration/modify-rich-text-configuration')]
    public function __invoke(AfterRichtextConfigurationPreparedEvent $event): void
    {
        $config = $event->getConfiguration();
        $config['editor']['config']['debug'] = true;
        $event->setConfiguration($config);
    }
}
Copied!

Impact 

It is now possible to modify the rich-text configuration after it has been fetched and prepared.

Feature: #107343 - PSR-14 to manipulate form creation process 

See forge#107343

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] .

The new event is dispatched immediately before a new form is created in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the new form

Example 

An example event listener could look like:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent;

final class MyEventListener
{
    #[AsEventListener('my_extension/before-form-is-created')]
    public function __invoke(BeforeFormIsCreatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsCreatedEvent , it is now possible to modify a form before it is created.

Feature: #107358 - Introduce Fluid page title ViewHelper 

See forge#107358

Description 

A new Fluid ViewHelper <f:page.title> has been introduced to allow setting the page title directly from Fluid templates.

This is especially useful for Extbase plugins that need to set a page title in their detail views without having to implement a custom page title provider.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.title>{item.title}</f:page.title>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper can also be used with static content:

EXT:my_extension/Resources/Private/Templates/Static/About.html
<f:page.title>About Us - Company Information</f:page.title>

<h1>About Us</h1>
<p>Welcome to our company...</p>
Copied!

Impact 

Extension developers can now easily set page titles from their Fluid templates without creating custom page title providers for each extension. This simplifies the implementation of dynamic page titles in Fluid templates or in Extbase plugins for detail views where the title should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing page title provider system and respects the configured provider priorities.

Feature: #107359 - Introduce Fluid page meta ViewHelper 

See forge#107359

Description 

A new Fluid ViewHelper <f:page.meta> has been introduced to allow setting meta tags directly from Fluid templates using TYPO3's MetaTagManager API.

This is especially useful for Extbase plugins that need to set meta tags in their detail views without having to implement custom meta tag handling.

In addition, frontend developers do not need to write custom TypoScript anymore but can use Fluid directly.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.meta property="description">
    {item.description}
</f:page.meta>
<f:page.meta property="og:title">
    {item.title}
</f:page.meta>
<f:page.meta property="og:type">
    article
</f:page.meta>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper supports all features of the MetaTagManager API:

OpenGraph and Twitter / X Card meta tags:

<f:page.meta property="og:title">
    My Article Title
</f:page.meta>
<f:page.meta property="og:description">
    Article description
</f:page.meta>
<f:page.meta property="twitter:card">
    summary_large_image
</f:page.meta>
Copied!

Sub-properties for complex meta tags:

<f:page.meta
    property="og:image"
    subProperties="{width: 1200, height: 630, alt: 'Article image'}"
>
    {item.image.url}
</f:page.meta>
Copied!

Custom meta tag types:

<f:page.meta property="author" type="name">
    John Doe
</f:page.meta>
<f:page.meta property="robots" type="name">
    index, follow
</f:page.meta>
Copied!

Replacing existing meta tags:

<f:page.meta property="description" replace="true">
    Override any existing description
</f:page.meta>
Copied!

ViewHelper Arguments 

  • property (required): The meta property name (for example, description, og:title)
  • type (optional): The meta type attribute (name, property, http-equiv). If not set, the appropriate MetaTagManager determines the type automatically.
  • subProperties (optional): Array of sub-properties for complex meta tags.
  • replace (optional): Boolean to replace existing meta tags with the same property (default: false).

Impact 

Extension developers can now easily set meta tags from Fluid templates without needing to use the MetaTagManager API directly in PHP code. This simplifies the implementation of SEO-optimized pages in Extbase plugins, especially for detail views where meta tags should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing MetaTagManager system and respects all configured meta tag managers and their priorities.

Feature: #107380 - PSR-14 to manipulate form duplication process 

See forge#107380

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDuplicate'] .

The new event is dispatched immediately before a form is duplicated in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the duplicated form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent;

final class BeforeFormIsDuplicatedEventListener
{
    #[AsEventListener('my_extension/before-form-is-duplicated')]
    public function __invoke(BeforeFormIsDuplicatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsDuplicatedEvent , it is now possible to modify a form before it is duplicated.

Feature: #107382 - PSR-14 before form deletion 

See forge#107382

Description 

A new PSR-14 event BeforeFormIsDeletedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] .

The new event is dispatched immediately before a form is deleted in the backend.

The event provides the following public properties:

  • $formPersistenceIdentifier: The form persistence identifier (read-only)
  • $preventDeletion: A boolean flag that can be set to true to prevent the deletion of the form

The new event is stoppable. As soon as $preventDeletion is set to true, no further listener is called.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent;

final class BeforeFormIsDeletedEventListener
{
    #[AsEventListener('my_extension/before-form-is-deleted')]
    public function __invoke(BeforeFormIsDeletedEvent $event): void
    {
        $event->preventDeletion = true;
        $persistenceIdentifier = $event->formPersistenceIdentifier;
        // Do something with the persistence identifier
    }
}
Copied!

Impact 

With the new BeforeFormIsDeletedEvent , it is now possible to prevent the deletion of a form and to add custom logic based on the delete action.

Feature: #107388 - PSR-14 to manipulate form before it is saved 

See forge#107388

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] .

The new event is dispatched immediately before a form is saved in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent;

final class BeforeFormIsSavedEventListener
{
    #[AsEventListener('my-extension/before-form-is-saved')]
    public function __invoke(BeforeFormIsSavedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsSavedEvent , it is now possible to modify a form definition as well as the form persistence identifier before it is saved.

Feature: #107435 - File replace modal with enhanced file information 

See forge#107435

Description 

The file replacement functionality has been significantly improved to provide a better user experience in the TYPO3 backend. Instead of opening the file replacement interface in a new window, it now opens in a modal dialog, maintaining the user's context and providing a more consistent experience with other TYPO3 backend operations.

The modal displays thumbnails for supported file types, allowing users to visually verify the current file before replacement. Additionally, the following detailed information is shown:

  • File type
  • File size
  • Creation date

Impact 

This enhancement significantly improves the file management experience in TYPO3 by providing a more modern, context-aware interface for file replacement. The combination of modal-based interaction and file information display creates a more efficient and user-friendly workflow that aligns with modern web application patterns.

The changes particularly benefit content editors who frequently work with files, providing them with the visual and metadata context needed to make confident file replacement decisions without interrupting their content creation workflow.

Feature: #107436 - Symfony Translation Component integration 

See forge#107436

Description 

TYPO3 now utilizes the Symfony Translation component for reading localization label files such as XLIFF and PO, instead of its custom localization parsers.

The migration brings several improvements:

  • Standardized file parsing using Symfony's translation loaders
  • Enhanced API for accessing translation catalogues
  • Support for custom translation loaders following Symfony standards

The new system maintains backward compatibility while providing a modern foundation for future improvements with translatable labels.

In addition, all label-related configuration options have been streamlined under the $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace.

The following new configuration options have been introduced:

$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']
Configure custom translation loaders.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['format']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales']
Moved from EXTCONF.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']
Moved from SYS.locallangXMLOverride.

Custom translation loaders 

Extension developers can now implement custom translation loaders by implementing Symfony's translation loader interfaces:

Example custom loader
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;

class CustomLoader implements LoaderInterface
{
    public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
    {
        // Custom loading logic
        $catalogue = new MessageCatalogue($locale);
        // ... populate catalogue
        return $catalogue;
    }
}
Copied!

Register custom loaders via configuration:

Register custom loader in configuration
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['fileEnding']
    = \MyVendor\MyExtension\Translation\CustomLoader::class;
Copied!

Impact 

All previous configuration options have been moved to the new $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace. These are automatically migrated to the new location when accessing the install tool.

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Feature: #107441 - Allow more hashing algorithms in FAL 

See forge#107441

Description 

Previously, FAL's LocalDriver only supported md5 and sha1 as hashing algorithms. While this may be sufficient for many use cases, it might be necessary to use different hashing algorithms depending on the specific scenario.

The method LocalDriver->hash() is now able to use any hashing algorithm that is registered in PHP itself by building a HashContext object and updating it by streaming the file content.

Impact 

FAL's LocalDriver can now make use of different hashing algorithms, e.g. crc32, sha256 and many more.

Feature: #107471 - New scheduler task wizard 

See forge#107471

Description 

A wizard to create new scheduler tasks has been introduced in the Administration > Scheduler module to significantly improve the user experience when creating new scheduler tasks. The wizard replaces the previous dropdown-based task selection in FormEngine with a modern, categorized interface similar to the content element wizard.

UX improvements 

  • Categorized task selection: Tasks are now organized by extension or category for better discoverability
  • Search functionality: Users can search and filter available tasks
  • Visual task representation: Each task displays with proper icons, titles, and descriptions

Technical improvements 

  • Prevents broken records: The old system preselected the first available task type when creating new tasks, which caused validation issues when users changed the task type, since required fields for the new type might not be properly initialized
  • Clean task type preselection: The selected task type is directly passed to FormEngine, eliminating the need to change the type in the form
  • Extensible via PSR-14 event: Extensions can modify wizard items through the new ModifyNewSchedulerTaskWizardItemsEvent

PSR-14 event 

A new PSR-14 event ModifyNewSchedulerTaskWizardItemsEvent has been introduced to allow extensions to modify the wizard items.

The event provides the following methods:

  • getWizardItems(): Returns the current wizard items array
  • setWizardItems(): Sets the complete wizard items array
  • addWizardItem(): Adds a single wizard item
  • removeWizardItem(): Removes a wizard item by key
  • getRequest(): Returns the current server request

Example 

A corresponding event listener class:

Example event listener class
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Scheduler\Event\ModifyNewSchedulerTaskWizardItemsEvent;

final class ModifySchedulerTaskWizardListener
{
    #[AsEventListener('my-extension/scheduler/modify-wizard-items')]
    public function __invoke(ModifyNewSchedulerTaskWizardItemsEvent $event): void
    {
        // Add a custom task to the wizard
        $event->addWizardItem('my_custom_task', [
            'title' => 'My Custom Task',
            'description' => 'A custom task provided by my extension',
            'iconIdentifier' => 'my-custom-icon',
            'taskType' => 'MyVendor\\MyExtension\\Task\\CustomTask',
            'taskClass' => 'MyVendor\\MyExtension\\Task\\CustomTask',
        ]);

        // Remove an existing task
        $event->removeWizardItem('redirects_redirects:checkintegrity');

        // Modify existing wizard items
        $wizardItems = $event->getWizardItems();
        foreach ($wizardItems as $key => $item) {
            if (isset($item['title'])
                && str_contains($item['title'], 'referenceindex:update')) {
                $item['title'] = 'Update reference index';
                $event->addWizardItem($key, $item);
            }
        }
    }
}
Copied!

Impact 

  • The scheduler task creation workflow is significantly improved with better UX
  • The risk of creating broken task records due to task type changes is eliminated
  • Extensions can easily modify the wizard through the PSR-14 event
  • The interface is more consistent with other TYPO3 wizard interfaces
  • Task discovery is improved through categorization and search functionality

Feature: #107488 - Extensible scheduler task timing options 

See forge#107488

Description 

The scheduler task timing configuration has been migrated to the single TCA execution_details field, which is of type json. This field has now been enhanced to allow extensions to customize the corresponding timing-related fields, particularly the frequency field with custom cron expressions. This is achieved through the new overrideFieldTca option, available in the TCA field configuration.

Previously, frequency options were only configurable through the global $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions'] array, which has been moved to the TCA configuration to provide better extensibility and consistency with TYPO3's configuration patterns, see Breaking: #107488 - Scheduler frequency options moved to TCA.

Enhanced customization options 

Extensions can now override any timing-related field configuration using the overrideFieldTca mechanism in the execution_details field.

Available fields:

  • Frequency field: frequency
  • Running type field: runningType
  • Parallel execution settings: multiple
  • Start or end date fields: start and end

Example usage 

Extensions can now add custom frequency options by creating a TCA override file:

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']
    ['overrideFieldTca']['frequency']['config']['valuePicker']['items'][] = [
        'value' => '0 2 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
    ];
Copied!

Extensions can also add a description:

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']
    ['overrideFieldTca']['multiple'] = [
        'description' => 'my_extension.messages:multiple.description',
    ];
Copied!

Impact 

This enhancement provides a more flexible and extensible way to configure scheduler task timing options, allowing extensions to seamlessly integrate custom timing configurations while maintaining consistency with TYPO3's configuration patterns.

Feature: #107518 - PSR-14 event to modify form elements after being added 

See forge#107518

Description 

A new PSR-14 event BeforeRenderableIsAddedToFormEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] .

The new event is dispatched immediately before a renderable has been constructed and added to the corresponding form. This allows full customization of the renderable after it has been initialized.

The event provides the $renderable public property.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent;

final class BeforeRenderableIsAddedToFormEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-added-to-form-event')]
    public function __invoke(BeforeRenderableIsAddedToFormEvent $event): void
    {
        $event->renderable->setLabel('foo');
    }
}
Copied!

Impact 

With the new BeforeRenderableIsAddedToFormEvent , it is now possible to modify a renderable after it has been initialized and right before being added to its form.

Feature: #107519 - Add "discard" command to DataHandler 

See forge#107519

Description 

The \TYPO3\CMS\Core\DataHandling\DataHandler PHP API has been extended with a new "discard" command to simplify workspace management.

This new command provides a cleaner, more explicit way to discard workspace records compared to the previous approach using version commands.

The new "discard" command can be used in the $commandArray parameter when calling the DataHandler to remove versioned records from a workspace.

Impact 

The "discard" command offers a more intuitive API for workspace operations:

  • Instead of using complex version commands with actions such as "clearWSID" or "flush", you can now use the straightforward "discard" command.
  • The command name clearly indicates its purpose.
  • The command handles all aspects of discarding workspace records, including any related child records.

Usage 

When using the discard command, it is crucial to use the UID of the versioned record (workspace version), not the UID of the live record.

Discarding a workspace record using DataHandler
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Example: Discard a versioned page record
$versionedPageUid = 123; // This must be the UID of the workspace version!

$commandArray = [
    'pages' => [
        $versionedPageUid => [
            'discard' => true,
        ],
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!
Discarding multiple records of different types
$commandArray = [
    'pages' => [
        456 => ['discard' => true], // Versioned page UID
    ],
    'tt_content' => [
        789 => ['discard' => true], // Versioned content element UID
        790 => ['discard' => true], // Another versioned content element UID
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!

Migration from legacy commands 

The new discard command replaces the previous version-based approach, which was not widely known:

Legacy approach (still supported but discouraged)
// Old way - will be removed in future versions
$commandArray = [
    'pages' => [
        $versionedUid => [
            'version' => [
                'action' => 'clearWSID',
            ],
        ],
    ],
];
Copied!
New recommended approach
// New way - cleaner and more explicit
$commandArray = [
    'pages' => [
        $versionedUid => [
            'discard' => true,
        ],
    ],
];
Copied!

The previous clearWSID and flush actions are still supported for backward compatibility but are considered deprecated and will be removed in future versions.

Feature: #107526 - Custom TCA types for scheduler tasks 

See forge#107526

Description 

Scheduler tasks can now be created using custom TCA types instead of the legacy :AdditionalFieldProvider approach. This enhancement allows developers to define custom database fields for specific task types, providing full flexibility of FormEngine and DataHandler for editing and persistence.

The new approach replaces three major downsides of the traditional method:

  1. Native database fields are now created automatically via TCA instead of being serialized into a JSON field.
  2. Tasks are registered via TCA record types instead of custom registration methods.
  3. Field handling is now done through standard TCA configuration, eliminating the need for AdditionalFieldProviders.

Benefits 

Using custom TCA types for scheduler tasks provides several advantages:

  • FormEngine handles validation automatically, reducing the risk of XSS or SQL injection vulnerabilities in extension code.
  • Task configuration follows standard TYPO3 form patterns.
  • Developers can use familiar TCA configuration instead of implementing custom field providers.
  • Access to all TCA field types, validation, and rendering options.

Migration 

Existing task types using custom TCA types automatically migrate existing data through the getTaskParameters() and setTaskParameters() methods:

  • During migration, getTaskParameters() is called to extract field values from the serialized task object.
  • For new TCA-based tasks, setTaskParameters() receives the full database record as an array instead of serialized data from the parameters field.
  • The task class name still matches the value of the tasktype field.

Implementation examples 

File storage indexing task 

TCA configuration in Configuration/TCA/Overrides/file_storage_indexing_task.php:

TCA configuration for the file storage indexing task
defined('TYPO3') or die();

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Scheduler\Task\FileStorageIndexingTask;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'scheduler.messages:fileStorageIndexing.name',
        'description' => 'scheduler.messages:fileStorageIndexing.description',
        'value' => FileStorageIndexingTask::class,
        'icon' => 'mimetypes-x-tx_scheduler_task_group',
        'iconOverlay' => 'content-clock',
        'group' => 'scheduler',
    ],
    '
        --div--;core.form.tabs:general,
            tasktype,
            task_group,
            description,
            file_storage;scheduler.messages:label.fileStorageIndexing.storage,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;core.form.tabs:access,
            disable,
        --div--;core.form.tabs:extended,',
    [],
    '',
    'tx_scheduler_task'
);
Copied!

Task class with migration support:

FileStorageIndexingTask class
namespace TYPO3\CMS\Scheduler\Task;

class FileStorageIndexingTask extends AbstractTask
{
    public $storageUid = -1;

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getTaskParameters(): array
    {
        return [
            'file_storage' => $this->storageUid,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $this->storageUid = $parameters['storageUid']
            ?? $parameters['file_storage']
            ?? 0;
    }
}
Copied!

Recycler cleaner task with custom fields 

TCA configuration with custom field overrides in Configuration/TCA/Overrides/scheduler_cleaner_task.php:

TCA configuration for the recycler cleaner task
defined('TYPO3') or die();

use TYPO3\CMS\Recycler\Task\CleanerTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType(
        [
            'label' => 'recycler.tasks:cleanerTaskTitle',
            'description' => 'recycler.tasks:cleanerTaskDescription',
            'value' => CleanerTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'iconOverlay' => 'content-clock',
            'group' => 'recycler',
        ],
        '
            --div--;core.form.tabs:general,
                tasktype,
                task_group,
                description,
                selected_tables;recycler.tasks:cleanerTaskTCA,
                number_of_days;recycler.tasks:cleanerTaskPeriod,
            --div--;scheduler.messages:scheduler.form.palettes.timing,
                execution_details,
                nextexecution,
                --palette--;;lastexecution,
            --div--;core.form.tabs:access,
                disable,
            --div--;core.form.tabs:extended,',
        [
            'columnsOverrides' => [
                'selected_tables' => [
                    'label' => 'recycler.tasks:cleanerTaskTCA',
                    'config' => [
                        'type' => 'select',
                        'renderType' => 'selectMultipleSideBySide',
                        'size' => 10,
                        'minitems' => 1,
                        'maxitems' => 100,
                        'itemsProcFunc' =>
                            CleanerTask::class. '->getAllTcaTables',
                        'items' => [],
                    ],
                ],
            ],
        ],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Task class with migration support:

CleanerTask class
namespace TYPO3\CMS\Recycler\Task;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class CleanerTask extends AbstractTask
{
    protected int $period = 0;

    protected array $tcaTables = [];

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getAdditionalInformation()
    {
        $message = sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionTables'
            ),
            implode(', ', $this->tcaTables)
        );
        $message .= '; ';
        $message .= sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionDays'
            ),
            $this->period
        );
        return $message;
    }

    public function getTaskParameters(): array
    {
        return [
            'selected_tables' => implode(',', $this->tcaTables),
            'number_of_days' => $this->period,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $tcaTables = $parameters['RecyclerCleanerTCA']
            ?? $parameters['selected_tables']
            ?? [];
        if (is_string($tcaTables)) {
            $tcaTables = GeneralUtility::trimExplode(',', $tcaTables, true);
        }
        $this->tcaTables = $tcaTables;
        $this->period = (int)(
            $parameters['RecyclerCleanerPeriod']
            ?? $parameters['number_of_days']
            ?? 180
        );
    }
}
Copied!

Creating custom task types 

To create a custom scheduler task with TCA configuration:

  1. Create the task class extending AbstractTask. Implement the core logic in the execute() method.
  2. Implement migration methods getTaskParameters() and setTaskParameters().
  3. Optionally add getAdditionalInformation() for backend listing output.
  4. Add TCA configuration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php.
  5. Define custom fields in the TCA showitem configuration.
  6. Register the task type using ExtensionManagementUtility::addRecordType().

Example custom task TCA registration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php:

Example custom task TCA registration
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTCAcolumns(
    'tx_scheduler_task',
    [
        'custom_field' => [
            'label' => 'my_extension.tasks:custom_field',
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'My Custom Task',
        'description' => 'Description of what this task does',
        'value' => \MyVendor\MyExtension\Task\CustomTask::class,
        'icon' => 'my-custom-icon',
        'iconOverlay' => 'my-custom-icon-overlay',
        'group' => 'my_extension',
    ],
    '
        --div--;General,
            tasktype,
            task_group,
            description,
            custom_field,
            number_of_days;my_extension.tasks:myTaskPeriodLabel,
        --div--;Timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;Access,
            disable,
    ',
    [
        'columnsOverrides' => [
            'number_of_days' => [
                'config' => [
                    'eval' => 'required',
                ],
            ],
        ],
    ],
    '',
    'tx_scheduler_task'
);
Copied!

Impact 

  • Extension developers can create more maintainable and secure scheduler tasks.
  • Custom task configuration benefits from full FormEngine capabilities.
  • Existing tasks are automatically migrated without data loss.
  • Development follows standard TYPO3 TCA patterns.
  • Improved validation and security through FormEngine and DataHandler.

Feature: #107528 - PSR-14 event before renderable is removed from form 

See forge#107528

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] .

The new event is dispatched immediately before a renderable is deleted from the form.

The event provides the following public properties:

  • $renderable: The form element (read-only).
  • $preventRemoval: A boolean flag that can be set to true to prevent the removal of the renderable.

The event is stoppable. As soon as $preventRemoval is set to true, no further listeners are executed.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent;

class MyEventListener {

    #[AsEventListener(
        identifier: 'my-extension/before-renderable-is-removed-from-form-event',
    )]
    public function __invoke(BeforeRenderableIsRemovedFromFormEvent $event): void
    {
        $event->preventRemoval = true;
        $renderable = $event->renderable;
        // Custom logic before the renderable is removed
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRemovedFromFormEvent , it is now possible to prevent the deletion of a renderable and to add custom logic based on the deletion.

Feature: #107537 - System resource API for system file access and public URI generation 

See forge#107537

Description 

TYPO3 allows files to be configured for multiple purposes. For example, a logo to be shown on the login page, CSS and JavaScript files to be added to web pages, or icons to be used for records. Most of the time, these are specified using the EXT syntax to reference a file within an extension. However, it can sometimes also be useful to reference an external URL or a file from a FAL storage.

To achieve this, TYPO3 Core code previously needed to parse the specified configuration, identify the type (extension file, FAL, URL), and then generate a URL from it. While there was some API available for this purpose, it was incomplete and consisted of multiple parts that were not clearly named, making them easy to misuse. As a consequence, code in the TYPO3 Core differed depending on where the configuration was used.

For users, this could lead to issues when a specific resource syntax worked in one place (for example, in TypoScript) but not in another (for example, in a Fluid email template). Moreover, the generated URLs could be inconsistent — sometimes including a cache-busting addition and sometimes not.

Third-party developers struggled with a scattered API that was hard to use and understand, especially regarding which methods should be called, in what order, and how to maintain compatibility between Composer mode and classic mode.

The new System Resource API addresses these shortcomings and enables many additional features to be built on top of it.

The code for resource resolving and URL generation is now encapsulated in a precise and centralized API in one place. This not only makes it easier to maintain and fix bugs, but ensures that such fixes apply automatically to all parts of the system where resources are configured.

Before diving into the details of the API, some terminology is clarified, followed by a top-level overview.

Naming conventions and top-level overview 

A system resource is a file or folder within a TYPO3 project. This can be a:

  • package resource – a file within an extension
  • FAL resource – a file from a FAL storage
  • app resource – a file in the TYPO3 project folder
  • URI resource – a URL

Package resource 

A package resource can now be specified with a new syntax like this:

PKG:my-vendor/package-name:Resources/Public/Icons/Extension.svg

It consists of three parts, separated by a colon (:):

  1. PKG prefix
  2. Composer name of the package (also possible in classic mode for extensions that contain a composer.json)
  3. Relative path to the file within the package

The well-known EXT syntax can still be used for the time being:

EXT:ext_name/Resources/Public/Icons/Extension.svg

This syntax is not yet deprecated, but users are advised to switch to the new syntax for new projects.

App resource 

An app resource is a file or folder within your TYPO3 installation. Such files can also be specified using the PKG syntax, using the virtual name typo3/app as package name:

PKG:typo3/app:public/typo3temp/assets/style.css

By default, only access to a fixed set of folders is allowed: public/_assets, public/typo3temp/assets, and public/uploads. Additional allowed folders can be configured via:

$GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths']

FAL resource 

While users are encouraged to use package and app resources, it is also possible to specify files from FAL storages using the following syntax:

FAL:1:/identifier/of/file.svg
Copied!

It consists of three parts, separated by a colon (:):

  1. FAL prefix
  2. Storage ID (UID of the FAL storage record)
  3. Identifier of the file within this storage (for hierarchical storages, a slash-separated path)

URI resource 

A fully qualified URL (including HTTP(S) scheme) can be specified:

https://www.example.com/my/image.svg

URIs relative to the current host can be specified by prefixing them with URI: like so:

URI:/path/to/my/image.svg

The string after the URI: prefix must be a valid URI. This means, that TYPO3 will now throw an exception, rather than rendering an invalid URI to HTML, when an invalid URI is provided.

Legacy resource annotations 

The following legacy string representations can still be used, but they are deprecated and will be removed in future TYPO3 versions:

  • FAL resource (relative path to the default FAL storage): fileadmin/identifier/of/file.svg
  • App resource (relative path to the project’s public directory): typo3temp/assets/style.css
  • App resource (relative path to the project’s public directory): _assets/vite/foo.css

All representations mentioned here are resource identifiers. They are strings that uniquely identify a resource within the system.

API description 

The PHP API consists of two parts:

  1. Resource resolving
  2. URI generation

The result of resource resolving is an object (different objects for different resource types), which can then be passed to the API for URI generation.

Example PHP usage 

use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(string $resourceIdentifier, ServerRequestInterface $request): string
{
    $resource = $this->systemResourceFactory->createPublicResource($resourceIdentifier);
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

The SystemResourceFactory and an implementation of a system resource publisher are injected via dependency injection (DI). The resource publisher is referenced using the interface SystemResourcePublisherInterface .

In the renderUrl() method, the resourceFactory is used to obtain a resource object from a given resource identifier using createPublicResource(). This object is then passed to the generateUri() method of the resourcePublisher.

The method has a second required argument for the current request. If the code has no access to the current request, null can be passed instead. Passing null is discouraged and might be deprecated in future TYPO3 versions. For CLI commands, passing null is fine, but absolute URIs (including scheme and hostname) cannot be generated in this case.

Example Fluid usage 

A new <f:resource> ViewHelper converts a resource identifier into an object that can be passed to other ViewHelpers. Currently only <f:uri.resource> accepts such resource objects, but more (for example <f:image>) will support them in the future.

<html>
<head>
    <link href="{f:resource(identifier:'PKG:typo3/cms-backend:Resources/Public/Css/webfonts.css') -> f:uri.resource()}" rel="stylesheet" />
</head>
<body>
    <f:asset.css identifier="main.css" href="PKG:my-vendor/site-package:Resources/Public/Css/main.css"/>
    <img src="{typo3.systemConfiguration.backend.loginLogo -> f:resource() -> f:uri.resource()}" alt="Logo" height="41" width="150" />
</body>
Copied!

All resource identifiers mentioned above can be passed to existing ViewHelpers like <f:asset>.

Example TypoScript usage 

page.includeCSS.extensionResource = EXT:backend/Resources/Public/Css/webfonts.css
page.includeCSS.packageResource = PKG:my-vendor/ext-name:Resources/Public/Css/main.css
page.includeCSS.appResource = PKG:typo3/app:public/_assets/style.css
page.includeCSS.uriResource = https://www.example.com/css/main.css
page.includeCSSLibs.fal = FAL:1:/templates/css/main.css
Copied!

CSS and JavaScript files can be referenced using all resource identifiers mentioned above.

Impact 

All parts of the TYPO3 Core where URLs are generated from specified resources now use the new API. This means all of those places support the new PKG syntax and have cache busting applied automatically.

TYPO3 installations using other syntax than the supported ones need to migrate their resource references. Resource identifiers such as typo3conf/ext/my_ext/Resources/Public/Image.svg no longer work consistently throughout the system and must be replaced with EXT or PKG resource identifiers.

The benefits are:

  • Consistency throughout the system for system resource resolving and URL generation.
  • A more intuitive API that helps avoid mistakes, including security-related ones.
  • Consolidation of all code currently resolving system resources and generating URLs.
  • A foundation for future features such as publishing system resources to a CDN and letting TYPO3 generate CDN URLs directly instead of parsing HTML output.

Outlook 

This change is a major step forward, but further improvements are planned:

  • Implement flexible resource publishing in Composer and classic mode, allowing different publishing strategies and configuration of additional public resource folders or files.
  • Use this API in all areas that consume private resources, most notably Fluid template files.
  • Replace FAL storage for system assets completely by leveraging the app resource to contain templates, CSS files, logos, images, or even complete themes.

Feature: #107566 - PSR-14 event after current page is resolved 

See forge#107566

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] .

The new event is dispatched after the current page has been resolved.

The event provides the following public properties:

  • $currentPage: The current page.
  • $formRuntime: The form runtime object (read-only).
  • $lastDisplayedPage: The last displayed page (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent;

final class AfterCurrentPageIsResolvedEventListener
{
    #[AsEventListener('my-extension/after-current-page-is-resolved-event')]
    public function __invoke(AfterCurrentPageIsResolvedEvent $event): void
    {
        $event->currentPage->setRenderingOption('enabled', false);
    }
}
Copied!

Impact 

With the new AfterCurrentPageIsResolvedEvent , it is now possible to manipulate the current page after it has been resolved.

Feature: #107568 - PSR-14 event before renderable is validated 

See forge#107568

Description 

A new PSR-14 event BeforeRenderableIsValidatedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] .

The new event is dispatched right before a renderable is validated.

The event provides the following public properties:

  • $value: The submitted value of the renderable.
  • $formRuntime: The form runtime object (read-only).
  • $renderable: The renderable (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Error\Error;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Localization\TranslationService;
use TYPO3\CMS\Form\Event\BeforeRenderableIsValidatedEvent;

final class BeforeRenderableIsValidatedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-validated')]
    public function __invoke(BeforeRenderableIsValidatedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'AdvancedPassword') {
            return;
        }

        $elementValue = $event->value;
        if ($elementValue['password'] !== $elementValue['confirmation']) {
            $processingRule = $renderable
                ->getRootForm()
                ->getProcessingRule($renderable->getIdentifier());

            $processingRule->getProcessingMessages()->addError(
                GeneralUtility::makeInstance(
                    Error::class,
                    GeneralUtility::makeInstance(TranslationService::class)->translate('validation.error.1556283177', null, 'EXT:form/Resources/Private/Language/locallang.xlf'),
                    1556283177
                )
            );
        }

        $event->value = $elementValue['password'];
    }
}
Copied!

Impact 

With the new BeforeRenderableIsValidatedEvent , it is now possible to modify or validate the value of a renderable element before TYPO3 Core performs its built-in validation logic. This allows extensions to inject custom validation rules or preprocessing steps before standard validation runs.

Feature: #107569 - PSR-14 event before renderable is rendered 

See forge#107569

Description 

A new PSR-14 event BeforeRenderableIsRenderedEvent has been introduced. It serves as a replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] .

The new event is dispatched right before a renderable is rendered.

The event provides the following public properties:

  • $renderable: The form element.
  • $formRuntime: The form runtime.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent;

final class BeforeRenderableIsRenderedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-rendered')]
    public function __invoke(BeforeRenderableIsRenderedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'Date') {
            return;
        }

        $date = $event->formRuntime[$renderable->getIdentifier()];
        if ($date instanceof \DateTime) {
            $event->formRuntime[$renderable->getIdentifier()] =
                $date->format('Y-m-d');
        }
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRenderedEvent , it is now possible to modify a renderable before it is rendered or adjust the form runtime data before rendering.

Feature: #107581 - Improve handling of FinisherException 

See forge#107581

Description 

\FinisherException instances thrown during form processing are now caught within their respective finisher. Instead of resulting in a generic 503 error page, the exception is logged, and a user-friendly error message is displayed to the user, indicating that the form could not be submitted successfully.

The error message can be customized for each finisher using the new errorMessage option. Additionally, the newly introduced Error template can be overridden and customized.

Impact 

Users no longer see a 503 error page if a FinisherException occurs. Instead, they receive a clear, user-friendly message in the form frontend. All FinisherExceptions are logged for further analysis and debugging.

This change improves the user experience and makes error handling in forms more flexible and transparent.

Feature: #107628 - Improved backend module naming and structure 

See forge#107628

Description 

TYPO3 Core's backend module structure has been modernized with clearer, more intuitive naming conventions that better align with industry standards used by enterprise content management systems.

Module names should immediately convey their purpose to editors and administrators. This series of renamings improves clarity, discoverability, and reduces the cognitive load for both new and experienced users.

Clear and consistent naming is fundamental to good user experience. When comparing TYPO3 Core with other enterprise CMS platforms, it became evident that some of TYPO3 Core's module names were either too technical, ambiguous, or did not clearly communicate their purpose.

To summarize, the goals of this restructuring are:

  • Improved clarity: Module names immediately convey their purpose.
  • Industry alignment: Adopt naming conventions familiar to users of other enterprise CMS platforms.
  • Better discoverability: Help new users find functionality without extensive training.
  • Reduced cognitive load: Minimize confusion when navigating the backend.

Module renamings 

The following modules have been renamed.

Top-level modules 

Web => Content 

The top-level Web module has been renamed to Content.

Rationale: TYPO3 is a content management system. The primary workspace where editors create and manage content should be clearly labeled as such. The term "Web" was ambiguous and did not communicate the module's purpose. Users outside the TYPO3 ecosystem often did not understand what "Web" meant in this context.

Migration: Update module parent references from 'web' to 'content':

return [
    'my_module' => [
        'parent' => 'content',  // Previously: 'web'
    ],
];
Copied!

File => Media 

The top-level File module has been renamed to Media.

Rationale: The term "Media" clearly indicates the module's purpose of managing digital media files (images, videos, documents, audio files, etc.) within the CMS. The term "File" was too generic and technical, while "Media" is widely understood and commonly used in content management systems to refer to digital content assets.

Migration: Update module parent references from 'file' to 'media':

return [
    'my_module' => [
        'parent' => 'media',  // Previously: 'file'
    ],
];
Copied!

Site Management => Sites 

The top-level Site Management module has been renamed to Sites.

Rationale: The former label "Site Management" was long and formal, and did not align with TYPO3’s evolving, concise module naming strategy. The simplified name "Sites" improves scanability in the module menu and matches the naming of other top-level modules. It also better reflects the purpose of the module: providing an overview entry point for all configured (web)sites.

Migration: The top-level module identifier site is kept. No migration is necessary.

Admin (tools) <=> System 

The top-level module formerly known as Admin tools is now called Administration.

The purpose of this top-level module has changed. It now contains those modules useful to backend administrators, such as user and permission management, the Scheduler, and Integrations.

Most modules formerly found in Admin tools are now located in System.

Rationale: The top-level module Administration now contains modules that are used by backend administrators in their daily work. Modules that require system maintainer permissions are found in the module named System.

Migration: Modules generally accessible to backend administrators should be moved to the top-level module with the identifier admin. Modules that require system maintainer permissions or are mainly useful to system maintainers and DevOps should be moved to system.

return [
    'my_administration_module' => [
        'parent' => 'admin',  // Previously: 'system'
        'access' => 'admin',
    ],
    'my_maintainer_module' => [
        'parent' => 'system',  // Previously: 'tools'
        'access' => 'systemMaintainer',
    ],
];
Copied!

Second-level modules 

For modules, where the module identifier changed, the upgrade wizard "Migrate module permissions" migrates module level group and user permissions.

Page => Layout 

The second-level Page module has been renamed to Layout to better match its scope.

Rationale: The previous module name "Page" did not clearly convey the module’s purpose or workflow. TYPO3 provides multiple ways to interact with a page (e.g. structure, properties, preview), and the term "Page" alone did not describe which aspect was being managed. The renamed module "Layout" more accurately reflects what editors do inside the module: maintain the page layout, manage content elements, and organize them into the correct columns and grids. This provides clearer expectations, improves usability for new editors, and aligns the module name with modern TYPO3 workflows and terminology.

Migration: Since the module is just renamed, there are no migrations necessary.

List => Records 

The second-level List module has been renamed to Records to better convey its purpose and improve clarity.

Rationale: The term "List" is too generic and does not adequately communicate the module's purpose. While "List" could refer to any kind of enumeration or overview, the module actually provides structured access to database records appearing on a page. The new name "Records" is more specific and immediately communicates that this module is about working with data records—viewing, editing, and managing them at the database level.

The term "Records" also aligns with how the module is already described in its own interface ("List of database records") and better reflects the technical nature of the module's functionality. For users coming from other enterprise CMS platforms or database-driven systems, "Records" is a widely understood term that clearly indicates low-level data management capabilities.

This renaming reduces ambiguity and helps users—especially those new to TYPO3—understand that this module provides direct access to the underlying record structure, distinguishing it from the more content-focused Layout module.

Migration: The module identifier has been renamed from web_list to records. An alias is in place. However, use the new identifier when referecing.

- $this->uriBuilder->buildUriFromRoute('web_list');
+ $this->uriBuilder->buildUriFromRoute('records');
Copied!

View => Preview 

The second-level View module has been renamed to Preview to better match its scope. It has also been moved one position down after Records, as that module is considered more important for daily work.

Rationale: The term "Preview" is more precise, as it triggers a frontend preview and cannot be misunderstood as "viewing" a page in the backend context.

Migration: Since the module is already internally referred to as page_preview, no changes in referencing modules are required.

Workspaces => Publish 

The second-level Workspaces module has been renamed to Publish to better match its current scope.

Rationale: The initially introduced "Workspaces administration" tool has been reworked to move content through a publishing process in past versions. For this reason, it is now renamed to "Publish" and is only visible when inside a workspace.

Migration: The module has internally been renamed to workspaces_publish. A module alias is in place, so references to the old workspaces_admin identifier keep working as before, but it is recommended to adapt usages.

The upgrade wizard "Migrate module permissions" migrates backend user and group-level permissions for this module.

Settings => Setup 

The second level Settings module has been integrated into Setup.

Rationale: Combining the "Setup" and "Settings" gives a more concise view, since managing sites and site settings are often done as one task.

Migration: The module identifier site_settings has been removed, the existing actions edit, save and dump have been renamed to editSettings, saveSettings and dumpSettings as part of the site_configuration module identifier.

System > Backend Users => Administration > Users 

The second-level System > Backend Users module has been renamed to Administration > Users to better match its scope.

It has also been moved to the top of the to Administration top level menu as it is frequently used by administrators.

Rationale: The new name "Users" is shorter and easier to recognize in the module menu. While "Backend Users" was technically precise, the simpler term improves readability and usability, making the module easier to find for administrators performing common user management tasks.

Migration: The identifier backend_user_management is kept unchanged, no migration needed.

System > DB Check => System > Database 

The second-level DB Check module has been renamed to Database to better reflect its purpose.

Rationale: The name "Database" is clearer and avoids the ambiguity of the abbreviation "DB". The module now focuses solely on search and query functionality and no longer performs database integrity checks.

Migration: The module identifier has changed from system_dbint to system_database. An alias ensures backward compatibility. Use the new identifier when registering custom modules.

 return [
     'my_database_tool' => [
-        'parent' => 'system_dbint',
+        'parent' => 'system_database',
     ],
 ];
Copied!

Impact 

All renamed module identifiers maintain their previous names as aliases, ensuring full backward compatibility. Existing code, configurations, and third-party extensions continue to work without modification.

Developers are encouraged to update their code to use the new identifiers for consistency and clarity.

The modernized naming improves the overall user experience by making the backend more intuitive and easier to navigate, particularly for users familiar with other enterprise CMS platforms.

Feature: #107663 - Introduce submodule dependencies 

See forge#107663

Description 

Backend modules can now declare a dependency on their submodules using the new appearance['dependsOnSubmodules'] configuration option. When enabled, a module will automatically hide itself from the module menu if none of its submodules are available to the current user.

This feature enables container modules to adapt dynamically to the user's permissions and installed extensions, ensuring the module menu remains clean and only displays modules that provide actual functionality.

Example configuration 

EXT:my_extension/Configuration/Backend/Modules.php
use TYPO3\CMS\Info\Controller\InfoModuleController;

return [
    'content_status' => [
        'parent' => 'content',
        'access' => 'user',
        'path' => '/module/content/status',
        'iconIdentifier' => 'module-info',
        'labels' => 'backend.modules.status',
        'aliases' => ['web_info'],
        'navigationComponent' => '@typo3/backend/tree/page-tree-element',
        'appearance' => [
            'dependsOnSubmodules' => true,
        ],
        'showSubmoduleOverview' => true,
    ],
    'web_info_overview' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
    'web_info_translations' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
];
Copied!

In this example, the Content > Status module will only be shown in the module menu if at least one of its submodules (Overview, Translations) is available to the current user.

If all submodules are either disabled, removed, or the user lacks access permissions to them, the parent module will automatically be hidden from the module menu.

Impact 

Module menus become more intuitive and user-focused. Container modules equipped with appearance['dependsOnSubmodules'] intelligently adapt to the current context, appearing only when they offer actionable functionality to the user.

The Content > Status module leverages this feature to seamlessly disappear from the module menu when extensions are uninstalled or when users lack permissions to access its submodules, preventing dead-end navigation paths and enhancing the overall backend experience.

Feature: #107668 - Improve scheduler task group handling and display 

See forge#107668

Description 

The scheduler module has been enhanced with improved visual organization and quick editing capabilities for task groups. Task groups can now be assigned custom colors to improve visual distinction, and the group name has been made directly editable from the module view.

A new color field has been added to the tx_scheduler_task_group table, allowing administrators to assign hex color values to each task group.

UI improvements 

The scheduler module now provides several enhancements for task groups:

Color coding
Task groups can be assigned a color that is displayed as a left border on the group panel, similar to the page module label. This makes it easy to visually distinguish between different groups at a glance.
Quick edit link
The group name is now a clickable link that opens the group record for editing, providing direct access to the group name, color, and description fields without navigating through the list module.
Description display
Group descriptions are now displayed in the scheduler module directly below the group name, providing additional context about the purpose of each group.
Bold group names
Group names are now displayed in bold for better visual hierarchy and readability in the scheduler module interface.

Implementation details 

The color field is implemented as a TCA field of type 'color' with the following characteristics:

  • Stores standard hex color values (for example #FF8700).
  • Includes a value picker with 11 predefined colors aligned with the TYPO3 brand palette.

Predefined color options:

  • TYPO3 Orange (#FF8700)
  • White (#ffffff)
  • Gray (#808080)
  • Black (#000000)
  • Blue (#2671d9)
  • Purple (#5e4db2)
  • Teal (#2da8d2)
  • Green (#3cc38c)
  • Magenta (#c6398f)
  • Yellow (#ffbf00)
  • Red (#d13a2e)

These options can be customized by manipulating the $GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'] array in your TCA overrides file.

Add a custom color option in a TCA override file
$GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'][] = [
    'label' => 'My Color',
    'value' => '#ABCDEF',
];
Copied!

Impact 

These enhancements improve the usability of the scheduler module, particularly for installations with many task groups. The improvements allow administrators to:

  • Quickly identify related task groups through visual color coding.
  • Edit group properties directly from the scheduler module.
  • Organize tasks by category, priority, or purpose.
  • See group descriptions without opening the group record.
  • Improve visual navigation in large scheduler configurations.

The feature is fully backward compatible. Existing task groups without colors continue to work as before, displaying without a colored border.

Feature: #107679 - PSR-14 event for custom record retrieval in LinkBuilder 

See forge#107679

Description 

A new PSR-14 event \TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent has been introduced to retrieve a record using custom code in the \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

The event is dispatched with $record set to null. If an event listener retrieves a record from the database, it can assign the record as an array to $record. Doing so stops event propagation and skips the default record retrieval logic in \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

Custom event listeners must handle all aspects normally performed by DatabaseRecordLinkBuilder, such as record visibility, language overlay, or version overlay, if relevant.

The event provides the following public properties (all read-only, except for $record):

  • $linkDetails: Information about the link being processed.
  • $databaseTable: The name of the database table the record belongs to.
  • $typoscriptConfiguration: The full TypoScript link handler configuration.
  • $tsConfig: The full TSconfig link handler configuration.
  • $request: The current request object.
  • $record: The database record as an array (initially null).

Example 

An example event listener could look like:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent;

final class MyEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/before-database-record-link-resolved',
    )]
    public function __invoke(BeforeDatabaseRecordLinkResolvedEvent $event): void
    {
        // Retrieve the record from the database as an array
        $result = /* ... */;

        if ($result !== false) {
            // Setting the record stops event propagation and
            // skips the default record retrieval logic
            $event->record = $result;
        }
    }
}
Copied!

Impact 

This new event allows developers to implement custom record retrieval logic for links created with typolink, for example to apply custom access restrictions or fetch data from alternative sources before rendering a link.

Feature: #107683 - File storage tree items modification event and label support 

See forge#107683

Description 

Similar to the page tree functionality introduced in TYPO3 v12 and v13, the file storage tree now supports modification of tree items through the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent .

The event is dispatched in the file storage TreeController after the storage tree items have been resolved and prepared. It provides the current PSR-7 request as well as the collection of file storage tree items.

Additionally, labels can now be added to file storage tree nodes via user TSconfig, using the combined identifier of the folder:

EXT:my_extension/Configuration/user.tsconfig
options.folderTree.label.1:/campaigns/ {
    label = Main Storage
    color = #ff8700
}
Copied!

Labels and status information 

Similar to the page tree, labels and status information can be added to file storage tree nodes. These features significantly improve the clarity and accessibility of the file storage tree component:

  • Labels: Each node can support multiple labels, sorted by priority. The highest priority label takes precedence, and only its marker is rendered. All additional labels are added to the title attribute of the node.
  • Status information: Can be added through the event to provide additional visual feedback. Like labels, status information is sorted by priority. Only the highest priority status indicator is displayed, while all status labels are added to the title attribute.

Example event listener 

EXT:my_extension/Classes/Backend/EventListener/ModifyFileStorageTreeItems.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-file-storage-tree-items',
)]
final readonly class ModifyFileStorageTreeItems
{
    public function __invoke(AfterFileStorageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();

        foreach ($items as &$item) {
            // Add special label for storage with uid 1
            if ($item['resource']->getCombinedIdentifier() === '1:/campaigns/') {
                $item['labels'][] = new Label(
                    label: 'A label',
                    color: '#abcdef',
                    priority: 10,
                );
                $item['statusInformation'][] = new StatusInformation(
                    label: 'An important information',
                    severity: ContextualFeedbackSeverity::INFO,
                    priority: 10,
                    icon: 'content-info',
                );
            }
        }

        $event->setItems($items);
    }
}
Copied!

Impact 

It is now possible to modify the prepared file storage tree items before they are returned by the TreeController , using the new PSR-14 event AfterFileStorageTreeItemsPreparedEvent . Additionally, labels can be assigned to file storage tree nodes via user TSconfig.

Using these functionalities helps provide visual cues and improved accessibility for editors working with file storages and folders.

Feature: #107697 - Unified file creation in Element Browser 

See forge#107697

Description 

The file creation functionality in the TYPO3 backend has been significantly improved and unified to provide a consistent user experience. Previously, creating new files required navigating to a separate page, which resulted in context loss and an inconsistent interface compared to folder creation.

File creation is now fully integrated into the Element Browser pattern, similar to how folder creation works. This change brings a consistent, modern, and more intuitive workflow to file management.

Key improvements 

Unified interface
File creation now uses the same modal-based Element Browser interface as folder creation, providing a consistent look and feel across all file operations.
Single entry point
The new New File button provides access to all file creation methods: file upload with drag-and-drop, online media (YouTube, Vimeo), and text file creation — all in one unified interface.
Context preservation
The modal interface keeps users in context with the file storage tree visible, eliminating the need to navigate away from the current view.
Advanced upload handling
The new implementation uses the drag-uploader component, which provides sophisticated duplication handling (replace, rename, skip, or use existing), replacing the previous simple "override existing" checkbox.
File extension filtering
File creation forms are automatically filtered based on allowed file types and hidden when no valid file extensions are available for the current context.
Updated button labels
For consistency and clarity, button labels have been updated from Create Folder / Create File to New Folder / New File, better reflecting the nature of the actions.

How to use 

In the File List module, click the New File button in the document header toolbar to open a modal dialog. This modal provides three ways to add files:

  1. Upload files - Click Select & upload files to choose files. The drag-and-drop uploader supports advanced duplication handling, allowing you to choose whether to replace, rename, skip, or use existing files when conflicts occur.
  2. Add online media - Enter a URL from supported online media platforms (YouTube, Vimeo) to add media files directly from the web.
  3. Create text file - Create a new empty text file with supported extensions (based on system configuration).

The modal keeps the file storage tree visible, allowing users to navigate between folders without losing context. All three options are available in a single, unified interface, streamlining the file creation workflow.

The previous Upload file button has been removed.

Impact 

The unified file creation interface provides a significantly improved user experience through better context preservation, advanced duplication handling, and a consistent modal-based workflow.

Editors benefit from having all file creation options in one place, eliminating the need to navigate between different pages for different file operations. The modal-based approach keeps the file storage tree visible, making it easier to work with files across multiple folders within a single, continuous workflow.

Feature: #107710 - Support for XLIFF 2.x translation files 

See forge#107710

Description 

TYPO3 now supports both XLIFF 1.2 and XLIFF 2.x translation file formats. The XLIFF loader automatically detects which version is used and parses the file accordingly, making the transition seamless for integrators and extension authors.

XLIFF (XML Localization Interchange File Format) is an XML-based format for storing translatable content. While TYPO3 has traditionally used XLIFF 1.2, the XLIFF 2.x standard brings improvements in structure and simplification.

Version detection 

The XLIFF loader automatically detects the file version by examining:

  1. The XML namespace (urn:oasis:names:tc:xliff:document:2.0 for XLIFF 2.0)
  2. The version attribute in the root element

No configuration or manual intervention is required - both formats work transparently side by side.

Key differences between XLIFF 1.2 and XLIFF 2.x 

For integrators working with translation files, here are the main structural differences:

XLIFF 1.2 structure:

  • Uses <trans-unit> elements directly within <body>
  • Translation approval via approved attribute (yes/no)
  • Namespace: urn:oasis:names:tc:xliff:document:1.2

Example XLIFF 1.2:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="de">
        <body>
            <trans-unit id="button.submit" approved="yes">
                <source>Submit</source>
                <target>Absenden</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

XLIFF 2.0 structure:

  • Uses <unit> elements containing <segment> elements
  • Translation state via state attribute on <segment> (initial, translated, reviewed, final)
  • More granular and modern structure
  • Namespace: urn:oasis:names:tc:xliff:document:2.0

Example XLIFF 2.0:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0"
       srcLang="en" trgLang="de">
    <file id="f1">
        <unit id="button.submit">
            <segment state="final">
                <source>Submit</source>
                <target>Absenden</target>
            </segment>
        </unit>
    </file>
</xliff>
Copied!

Translation approval handling 

TYPO3's requireApprovedLocalizations configuration is respected for both formats:

  • XLIFF 1.2: Translations with approved="no" are skipped when approval is required
  • XLIFF 2.x: Translations with state="initial" or state="translated" are treated as not approved, while state="final" and state="reviewed" are considered approved

Impact 

Extension authors and integrators can now use either XLIFF 1.2 or XLIFF 2.x format for their translation files. Existing XLIFF 1.2 files continue to work without any changes, while new projects can leverage the more modern XLIFF 2.x standard.

This also improves compatibility with modern translation tools and services that have adopted the XLIFF 2.0 standard.

Feature: #107712 - Introduce card-based submodule overview 

See forge#107712

Description 

Backend modules can now display a card-based overview of their submodules instead of automatically redirecting to the first available submodule. This new showSubmoduleOverview configuration option enables a more user-friendly navigation experience, similar to the Install Tool's maintenance card layout.

When enabled, clicking on a second-level module displays an overview page with cards for each accessible submodule. Each card shows the module's icon, title, description, and an "Open module" button, allowing users to make an informed choice about which submodule to access.

Example configuration 

return [
    'web_info' => [
        'parent' => 'content',
        'showSubmoduleOverview' => true,
        // ...
    ],
    'web_info_overview' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/overview',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.overview',
        // ... routes and other configuration
    ],
    'web_info_translations' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/translations',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.translations',
        // ... routes and other configuration
    ],
];
Copied!

In this example, the Content > Status module displays a card-based overview showing both Pagetree Overview and Localization Overview submodules. Users can read the description of each module before deciding which one to open.

The feature works seamlessly with the existing module permission system. Only submodules that the current user has access to are displayed in the overview. If no accessible submodules exist, a helpful information message is shown instead of an empty page.

Implementation details 

The showSubmoduleOverview option modifies the behavior in several key areas:

  1. Module routing - When set to true, the module's default route targets SubmoduleOverviewController instead of automatically redirecting to the first available submodule.
  2. Middleware behavior - The BackendModuleValidator middleware skips its automatic submodule redirection logic when this option is enabled, allowing the overview page to be displayed.
  3. Navigation enhancement - The system provides automatic navigation capabilities:

    • The SubmoduleOverviewController displays the submodule jump menu, allowing quick access to all available submodules.
    • When showSubmoduleOverview is activated, the ModuleTemplate automatically adds a "Module Overview" menu item to the submodule dropdown.
    • This allows users to easily navigate back to the overview from any submodule, especially useful when a submodule does not manually provide a "go back" button.

To provide meaningful descriptions on the overview cards, modules should define a description or shortDescription in their labels configuration. These are displayed in the card body to help users understand each submodule's purpose.

Impact 

Backend navigation becomes more intuitive and self-documenting. Container modules using showSubmoduleOverview provide users with a clear overview of available functionality, eliminating the confusion of being automatically redirected to an arbitrary first submodule.

The Content > Status module now uses this feature to present its submodules in an accessible, visually organized manner. Users can quickly understand what each submodule offers before navigating to it, improving discoverability and user experience in the TYPO3 backend.

Feature: #107725 - Support username for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password. With this change, the Redis cache backend in TYPO3 now supports both authentication mechanisms.

You can configure the Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend'] = RedisBackend::class;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The RedisBackend now supports authentication using both a username and a password.

The password configuration option is now typed as array|string. Using an array for this configuration option is deprecated and will be removed in TYPO3 v15.0.

Feature: #107755 - Add creation information for redirects 

See forge#107755

Description 

The redirects component has been extended with additional meta information to improve audit trails and team collaboration. Redirects now automatically capture and display:

  • Creation timestamp
  • Creating backend user

After months or even years, teams frequently wonder why a particular redirect exists and who created it. Displaying the creator (backend user) and creation date directly in the backend module makes auditing and communication significantly easier.

Impact 

All newly created redirects automatically include creation information, regardless of whether they are created:

  • Manually through the redirects backend module
  • Automatically when updating a page slug
  • Programmatically through the DataHandler API

Existing redirects created before this feature will only show the creation date, as the user information was not tracked previously.

Feature: #107759 - TranslateViewHelper supports translation domain syntax 

See forge#107759

Description 

The Fluid <f:translate> ViewHelper now supports the new translation domain syntax, providing a more concise and readable way to reference translation labels.

A new domain attribute has been added that accepts both traditional extension names and the new translation domain names.

The ViewHelper now supports multiple ways to specify translations:

  1. New domain attribute - recommended for new code:

    <f:translate key="form.legend" domain="my_extension" />
    <f:translate key="form.legend" domain="my_extension.messages" />
    Copied!
  2. Inline domain syntax in key - shortest form:

    <f:translate key="my_extension.messages:form.legend" />
    <f:translate key="LLL:my_extension.messages:form.legend" />
    Copied!
  3. Traditional extensionName attribute - still supported:

    <f:translate key="form.legend" extensionName="MyExtension" />
    <f:translate key="form.legend" extensionName="my_extension" />
    Copied!
  4. Full LLL reference - classic syntax, still supported:

    <f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:form.legend" />
    Copied!

Domain attribute priority 

When both domain and extensionName are provided, the domain attribute takes precedence:

<!-- Uses "other_extension.messages" domain, not "MyExtension" -->
<f:translate key="label" domain="other_extension.messages" extensionName="MyExtension" />
Copied!

Automatic domain detection 

If neither domain nor extensionName are specified, the ViewHelper attempts to automatically detect the translation domain from the context:

  1. Extbase context: Uses the controller extension name
  2. Key with domain prefix: Extracts domain from key="domain:id"
  3. LLL reference: Parses the extension key from the file path

Examples 

Using domain attribute with full domain name 

You can specify the exact translation domain including resource names:

<f:translate key="menu.item" domain="backend.toolbar" />
<!-- Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf -->
Copied!

Inline domain syntax in key 

The shortest form combines domain and key directly:

<f:translate key="core.form.tabs:general" />
<!-- Resolves to: EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf -->
Copied!

With arguments and default values 

All existing ViewHelper features work with the new syntax:

<f:translate
    key="users"
    domain="backend.messages"
    arguments="{0: count}"
    default="No users found"
/>
Copied!

Using variables for domain 

Dynamic domain selection is supported:

<f:translate key="label.title" domain="{myDomain}" />
Copied!

Migration from extensionName 

Existing code using extensionName continues to work without changes. However, new code should prefer the domain attribute combined with translation domain syntax for better readability.

Before:

<f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang_form.xlf:legend" />
<f:translate key="legend" extensionName="MyExtension" />
Copied!

After:

<f:translate key="my_extension.form:legend" />
<f:translate key="legend" domain="my_extension.form" />
Copied!

Impact 

The <f:translate> ViewHelper now provides a more convenient and readable way to reference translations using the new translation domain syntax. This reduces verbosity in Fluid templates and aligns with modern translation system conventions used in Symfony and other frameworks.

All existing syntax forms remain fully supported, ensuring backward compatibility. The new syntax can be adopted incrementally within a project, and both old and new forms can coexist in the same template.

Feature: #107783 - Register metadata extractors via interface 

See forge#107783

Description 

Metadata extractors are service classes that are automatically executed whenever an asset or file is added to the FAL storage, or FAL indexing is executed.

Registration of metadata extractors now happens automatically when the required interface ExtractorInterface is implemented by the class, utilizing autoconfigure tagging provided by the Symfony Dependency Injection framework.

No further manual registration is required.

Additionally, the class ExtractorRegistry now uses strong type declarations, which should not affect public consumers. The interface remains unchanged in its type declarations.

Impact 

Instances of ExtractorInterface are now detected and registered automatically.

Feature: #107784 - Autoconfigure backend layout data providers 

See forge#107784

Description 

Backend layout providers are now autoconfigured once they implement the required DataProviderInterface . Each autoconfigured layout provider is tagged with page_layout.data_provider in the service container and is automatically added to the global DataProviderCollection , if autoconfiguration is enabled in Services.yaml or Services.php.

Since backend layout providers must be identifiable to establish a relation to a configured backend layout, the corresponding interface has been extended. It now requires backend layout providers to implement a new method getIdentifier().

Example 

EXT:my_extension/Classes/View/BackendLayout/MyLayoutDataProvider.php
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface;

final class MyLayoutDataProvider implements DataProviderInterface
{
    // ...

    public function getIdentifier(): string
    {
        return 'my_provider';
    }
}
Copied!

Manual service configuration 

If autoconfiguration is disabled, manually tag the service in Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
Copied!

Provider ordering 

If you need to control the order in which providers are processed, use service priorities in your Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
        priority: 100
Copied!

Impact 

Backend layout data providers are now automatically registered and can be used without further configuration. This improves developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] can no longer be used. Instead, existing backend layout providers must implement the new method getIdentifier().

Using the new autoconfigure-based approach, developers can still support multiple TYPO3 Core versions by keeping the legacy array-based approach next to the new autoconfigure-based configuration.

Feature: #107791 - Reports module uses native submodule overview 

See forge#107791

Description 

The Administration > Reports module has been refactored to use TYPO3 Core’s native submodule system with the showSubmoduleOverview feature instead of a custom report registration mechanism. This provides a more consistent user experience and aligns the Reports module with other TYPO3 backend modules.

The module now displays a card-based overview of available reports, similar to other modules like the Content > Status module. Each report is registered as a proper backend submodule in Configuration/Backend/Modules.php.

Changes 

Module structure:

  • Administration > Reports is now a container module with showSubmoduleOverview enabled.
  • Individual reports (Status, Record Statistics) are registered as submodules.
  • The native submodule overview automatically provides cards with icons, titles, and descriptions.

Benefits:

  • Automatic "Module Overview" menu item in the submodule dropdown.
  • Consistent navigation experience across all TYPO3 backend modules.
  • Simpler architecture without custom registration infrastructure.
  • Easier to extend - just add submodules to Modules.php.

Example 

Creating a custom report now follows the same mechanism as registering a backend submodule:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'system_reports_myreport' => [
        'parent' => 'system_reports',
        'access' => 'admin',
        'path' => '/module/system/reports/myreport',
        'iconIdentifier' => 'my-report-icon',
        'labels' => 'my_extension.module',
        'routes' => [
            '_default' => [
                'target' => \MyVendor\MyExtension\Controller\MyReportController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The controller returns a standard PSR-7 response with rendered content:

EXT:my_extension/Classes/Controller/MyReportController.php
use TYPO3\CMS\Core\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

#[AsController]
final readonly class MyReportController
{
    public function __construct(
        protected ModuleTemplateFactory $moduleTemplateFactory,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->moduleTemplateFactory->create($request);
        $view->assign('data', $this->collectReportData());
        return $view->renderResponse('MyReport');
    }
}
Copied!

Impact 

The Administration > Reports module now provides a cleaner and more intuitive interface using standard TYPO3 backend navigation patterns.

Extension developers can create custom reports by registering them as backend submodules, using the same module registration mechanisms already available in TYPO3, instead of implementing a separate custom report interface.

Feature: #107794 - Improved breadcrumb navigation in backend 

See forge#107794

Description 

The TYPO3 backend now provides contextual breadcrumb navigation in the document header of backend modules, helping users understand their current location and navigate back through hierarchies.

Breadcrumbs are automatically displayed when:

  • Editing records (pages, content elements, etc.)
  • Creating new records
  • Browsing file storages and folders
  • Working with multiple records simultaneously

The breadcrumb navigation includes the following features:

Smart context detection
Breadcrumbs automatically adapt based on what you are working with — whether it is a page, content element, file, or folder.
Hierarchical navigation
Click any breadcrumb item to navigate back to that level in the hierarchy. For pages, the complete page tree path is shown.
Module awareness
Breadcrumbs remember which module you are in and keep you in that module when navigating (for example, staying in the Info module instead of switching to the Page module).
Route preservation
When navigating through breadcrumbs, the current module action or sub-route is preserved (for example, remaining in edit view when clicking parent pages).
Responsive design
On smaller screens, breadcrumb items automatically collapse into a dropdown to save space while maintaining full functionality.

Impact 

Backend users benefit from improved navigation and orientation:

  • Always know where you are: The breadcrumb trail shows your current location in the page tree, file system, or record hierarchy.
  • Quick navigation: Jump back to any parent level with a single click instead of using the browser’s back button or tree navigation.
  • Context preservation: Stay within your current module when navigating through parent items.
  • Special states visible: When creating new records or editing multiple items, this is clearly indicated in the breadcrumb trail.

Examples 

Page editing
When editing page Contact in a site structure like Home → Company → Contact, the breadcrumb shows: HomeCompanyContact
Content creation
When creating a new content element on page About, the breadcrumb shows: HomeAboutCreate New Content Element
File management
When browsing fileadmin/images/products/ the breadcrumb shows: fileadminimagesproducts

For extension developers 

Setting basic breadcrumb context 

Custom backend modules can integrate breadcrumb navigation using new convenience methods on DocHeaderComponent :

// For page-based modules
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For record editing
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);

// For file/folder browsing
$view->getDocHeaderComponent()->setResourceBreadcrumb($file);
Copied!

These methods automatically generate appropriate breadcrumb trails including:

  • Page tree hierarchy for page-based modules
  • Parent pages for content records
  • Folder structure for file resources
  • Module hierarchy for third-level modules

Adding suffix nodes for special states 

The addBreadcrumbSuffixNode() method allows appending custom breadcrumb nodes after the main breadcrumb trail. This is useful for indicating special states or actions such as:

  • “Create New” actions when creating records
  • “Edit Multiple” states when editing multiple records
  • Custom contextual information specific to the current view

Example: Adding a "Create New" suffix node

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$view = $this->moduleTemplateFactory->create($request);
$docHeader = $view->getDocHeaderComponent();

// Set main breadcrumb context (for example current page)
$docHeader->setPageBreadcrumb($pageInfo);

// Add suffix node for "Create New" action
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'new',
        label: 'Create New Content Element',
        icon: 'actions-add'
    )
);
Copied!

Example: Multiple suffix nodes

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$docHeader = $view->getDocHeaderComponent();
$docHeader->setRecordBreadcrumb('pages', $pageUid);

// First suffix: editing mode
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'edit',
        label: 'Edit',
        icon: 'actions-document-open'
    )
);

// Second suffix: specific field
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'field',
        label: 'Page Properties'
    )
);
Copied!

Example: Clickable suffix nodes

Suffix nodes can also be clickable by providing a URL:

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute(
    'web_layout',
    ['id' => $pageUid, 'mode' => 'preview']
);

$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'preview',
        label: 'Preview Mode',
        icon: 'actions-view',
        url: $url
    )
);
Copied!

Deprecation notice 

The previous setMetaInformation() method has been deprecated in favor of the new breadcrumb API. See Deprecation: #107813 - Deprecate MetaInformation API in DocHeader for migration instructions.

Feature: #107795 - Introduce Integrations module 

See forge#107795

Description 

The new Integrations module has been introduced in the TYPO3 backend under the Administration section. This module serves as the central hub for connecting TYPO3 with external systems and third-party services.

The module uses the card-based submodule overview feature to provide an intuitive and organized interface for managing different types of integrations. At present, it consolidates the following existing modules as third-level submodules:

  • Webhooks - Manage outgoing HTTP webhooks to external systems
  • Reactions - Manage incoming HTTP webhooks from external systems

The Integrations module uses a three-level hierarchy structure:

  • Administration (main module)
  • Integrations (second-level parent module with card-based overview)
  • Webhooks / Reactions (third-level modules)

Module navigation 

The third-level modules (Administration > Integrations > Webhooks and :guilabel:`Administration > Integrations > Reactions) now include:

  • Doc header module menu - Quick navigation dropdown to switch between submodules or return to the Integrations overview
  • Go back button - Direct link to return to the Integrations overview

Backward compatibility 

The existing module identifiers continue to work through aliases:

  • webhooks_management redirects to integrations_webhooks
  • system_reactions redirects to integrations_reactions

Impact 

The new Administration > Integrations module provides a centralized location for managing all types of external system integrations in TYPO3. This improves backend organization and user experience by grouping related functionality together.

The module is designed to be extensible, allowing future integration types, such as translation services, AI platforms, or other external tools, to be added as additional third-level modules within the Integrations hub.

Feature: #107812: Setting to restrict Latest changed Pages Widget to current user's changes 

See forge#107812

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing Latest changed Pages widget can now be configured to display only pages that have been changed by the current TYPO3 user.

Only show my changes

Impact 

  • TYPO3 users can use the Latest changed Pages widget to quickly resume their recent work.
  • Existing setups are unaffected unless this option is explicitly enabled.

Feature: #107823 - ComponentFactory for backend components 

See forge#107823

Description 

A new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class has been introduced as the central location for all backend component creation. It provides factory methods for buttons and menu components, offering both pre-configured buttons for common patterns and basic component creation methods.

The ComponentFactory serves multiple purposes:

  1. Pre-configured common buttons – Ready-to-use buttons like back, close, save, reload, and view with standardized icons, labels, and behavior.
  2. Basic button creation – Factory methods for creating button instances (previously available only on ButtonBar).
  3. Menu component creation – Factory methods for creating Menu and MenuItem instances (previously available only on MenuRegistry and Menu).

The deprecated ButtonBar::make*(), Menu::makeMenuItem(), and MenuRegistry::makeMenu() methods have been replaced by ComponentFactory, providing a cleaner separation of concerns where container classes manage organization and ComponentFactory handles component creation.

Additionally, several "add" methods now support fluent interface patterns to enable method chaining for improved code readability.

Database record list and file list 

The \TYPO3\CMS\Backend\RecordList\DatabaseRecordList and \TYPO3\CMS\Filelist\FileList classes, which are responsible in the backend to show all lists of records and files, now make use of the ComponentFactory. The list of "action buttons" is no longer represented with plain HTML and allows a clear distinction of primary and secondary actions.

For this, the new enum \TYPO3\CMS\Backend\Template\Components\ActionGroup and \TYPO3\CMS\Backend\Template\Components\ComponentGroup are also added to the Button API to allow this grouping.

The existing PSR-14 events ( \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent , \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent ) which are used within these classes have been streamlined to deal with these API changes. Details can be found in the related breaking changes document of forge#107884.

The Button API has also been enhanced to allow passing a new \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum to differentiate the buttons for certain icon sizes.

Available Factory Methods 

The ComponentFactory provides two categories of methods:

Pre-configured common buttons:

These methods provide ready-to-use buttons with sensible defaults. The returned instances are fully mutable and can be further customized using fluent interface methods (for example, setDataAttributes(), setClasses(), setIcon()).

URL parameters accept both string and UriInterface for convenience.

  • createBackButton(string|UriInterface $returnUrl) – Standard back navigation with "Go back" label.
  • createCloseButton(string|UriInterface $closeUrl) – Close button for modal-like views.
  • createSaveButton(string $formName = '') – Standard save button for forms.
  • createReloadButton(string|UriInterface $requestUri) – Reload current view.
  • createViewButton(array $previewDataAttributes = []) – View/preview page button with data attributes.

Basic button creation:

  • createLinkButton() – Creates a new LinkButton instance.
  • createInputButton() – Creates a new InputButton instance.
  • createGenericButton() – Creates a new GenericButton instance.
  • createSplitButton() – Creates a new SplitButton instance.
  • createDropDownButton() – Creates a new DropDownButton instance.
  • createFullyRenderedButton() – Creates a new FullyRenderedButton instance.
  • createShortcutButton() – Creates a new ShortcutButton instance.
  • createDropDownDivider() – Creates a new DropDownDivider instance.
  • createDropDownItem() – Creates a new DropDownItem instance.

Menu component creation:

  • createMenu() – Creates a new Menu instance.
  • createMenuItem() – Creates a new MenuItem instance.

The Button API has also been enhanced to allow passing a new enum to differentiate the buttons for certain icon sizes.

Improvements to Button API types 

The following button types can use getSize() and setSize() methods in their instance to set the icon size with the \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum, choosing between a small and medium variant (utilizing CSS classes internally):

  • \TYPO3\CMS\Backend\Template\Components\Buttons\DropDownButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton

Impact 

Backend module developers should now inject ComponentFactory in their controllers to create buttons. The factory provides:

  1. Pre-configured buttons for common patterns (back, close, save, reload, view).
  2. Basic button creation methods (formerly only available on ButtonBar).

The ButtonBar::make*() methods continue to work but are deprecated and will be removed in TYPO3 v15. This change provides a cleaner architecture where ComponentFactory handles all button creation and ButtonBar focuses solely on managing button positioning and organization.

Example – Using pre-configured buttons (inject ComponentFactory):

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// In controller constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// In controller action
public function editAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    // Use pre-configured back button
    $backButton = $this->componentFactory->createBackButton($returnUrl);
    $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1);

    // Use pre-configured save button
    $saveButton = $this->componentFactory->createSaveButton('editform');
    $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);

    // ...
}
Copied!

Example – Creating basic buttons via ComponentFactory :

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Inject ComponentFactory in constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// Create basic buttons via factory
$linkButton = $this->componentFactory->createLinkButton()
    ->setHref($url)
    ->setTitle('Custom')
    ->setIcon($icon);
Copied!

Example - Using the ModuleTemplate convenience method:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Shorthand: use ModuleTemplate::addButtonToButtonBar()
    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createReloadButton(
            $this->request->getUri()->getPath()
        ),
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createBackButton($returnUrl),
        ButtonBar::BUTTON_POSITION_LEFT,
        1
    );

    // ...
}
Copied!

Example - Customizing pre-configured buttons:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Pre-configured buttons return mutable instances that can be further
    // customized.
    $reloadButton = $this->componentFactory
        ->createReloadButton(
            (string)$this->uriBuilder->buildUriFromRoute(
                $currentModule->getIdentifier()
            )
        )
        ->setDataAttributes(['csp-reports-handler' => 'refresh']);

    $this->moduleTemplate->addButtonToButtonBar(
        $reloadButton,
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    // Add custom styling or behavior to a save button
    $saveButton = $this->componentFactory
        ->createSaveButton('myform')
        ->setClasses('btn-primary custom-save')
        ->setDataAttributes(['validate' => 'true']);

    $this->moduleTemplate->addButtonToButtonBar(
        $saveButton,
        ButtonBar::BUTTON_POSITION_LEFT
    );

    // URL parameters accept both string and UriInterface
    $backButton = $this->componentFactory->createBackButton(
        $this->request->getUri()
    ); // UriInterface
    // or
    $backButton = $this->componentFactory->createBackButton(
        '/return/url'
    ); // string

    // ...
}
Copied!

Fluent Interface Improvements 

Several "add" methods now support fluent interface patterns, enabling method chaining:

ButtonBar

$buttonBar
    ->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1)
    ->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
Copied!

Menu

$menu
    ->addMenuItem($listItem)
    ->addMenuItem($gridItem)
    ->addMenuItem($tableItem);
Copied!

MenuRegistry

$menuRegistry
    ->addMenu($viewMenu)
    ->addMenu($filterMenu);
Copied!

These changes provide a more fluent API while maintaining backward compatibility, as the return values were previously ignored ( void).

Design Rationale 

Why fluent interface instead of named parameters?

The ComponentFactory intentionally uses a fluent interface approach (chained method calls) rather than accepting parameters in factory methods. This design decision was made for several important reasons:

Consistency with TYPO3 patterns
The fluent interface pattern is well-established throughout TYPO3's codebase and familiar to extension developers. Introducing a different pattern here would be inconsistent with the rest of the framework.
Diverse button types with different properties
Different button types have vastly different configuration requirements. For example, InputButton needs name/value/form attributes, LinkButton needs href attributes, DropDownButton needs items, and SplitButton needs primary and secondary actions. A unified parameter-based approach does not fit this diversity well and would lead to confusing method signatures with many optional parameters.
Pre-configured buttons solve common cases
The factory already provides pre-configured methods like createSaveButton(), createBackButton(), createCloseButton(), createReloadButton(), and createViewButton() that handle the most common use cases with minimal code.
Avoids duplication and maintenance burden
A parameter-based approach would require duplicating all button-specific configuration knowledge in both the button classes and the factory methods. This creates a maintenance burden where changes to a button's properties must be reflected in multiple locations.
Keeps factory simple and maintainable
By keeping factory methods focused on instantiation, the ComponentFactory remains simple, maintainable, and easy to extend. Each button class maintains complete ownership of its own configuration logic.

Fluent interface is already concise

// Common case – use pre-configured button
$saveButton = $this->componentFactory->createSaveButton('myform');

// Custom case - fluent interface is clear and flexible
$customButton = $this->componentFactory->createInputButton()
    ->setName('custom_action')
    ->setValue('1')
    ->setTitle('Custom Action')
    ->setIcon(
        $this->iconFactory->getIcon('actions-heart', IconSize::SMALL)
    );
Copied!

This design ensures the API remains maintainable, consistent with TYPO3 conventions and suitable for the diverse requirements of different button types while still providing convenience methods for common patterns.

Feature: #107871 - Autoconfigure backend avatar providers 

See forge#107871

Description 

Backend avatar providers must either use the PHP attribute #[AsAvatarProvider] or be manually tagged in the service container with backend.avatar_provider.

When autoconfiguration is enabled in Services.yaml or Services.php, applying #[AsAvatarProvider] will automatically add the backend.avatar_provider tag. Otherwise, the tag must be configured manually.

Example 

EXT:my_extension/Classes/Backend/Avatar/MyAvatarProvider.php
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
use TYPO3\CMS\Backend\Backend\Avatar\AvatarProviderInterface;

#[AsAvatarProvider(
    'my_provider',
    before: ['provider-1'],
    after: ['provider-2']
)]
final class MyAvatarProvider implements AvatarProviderInterface
{
    // ...
}
Copied!

Impact 

Backend avatar providers are now automatically registered using the PHP attribute #[AsAvatarProvider]. This improves the developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] can no longer be used.

To support multiple TYPO3 Core versions simultaneously, extensions may still implement the legacy array-based registration alongside the new autoconfiguration-based approach.

Feature: #107873 - New Bookmarks Widget 

See forge#107873

Description 

A new Bookmarks dashboard widget has been added to display the current user's bookmarks directly in the TYPO3 Dashboard. This allows editors and administrators to quickly access frequently used pages, records, or modules without navigating through the backend menu.

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the Bookmarks widget supports the following settings:

Widget Label
Custom title for the widget instance. If left empty and a specific group is selected, the group title is used as widget label automatically
Group
Filter bookmarks by a specific bookmark group, or show all groups.
Limit
Maximum number of bookmarks to display (default: 10).

Impact 

  • Editors can add the Bookmarks widget to their dashboard for quick access to their saved bookmarks.
  • Each widget instance can be configured independently, allowing multiple Bookmarks widgets with different group filters on the same dashboard.
  • Existing bookmarks are displayed automatically without additional setup.

Feature: #107875 - Improved DocHeader layout and unified language selector 

See forge#107875

Description 

The TYPO3 backend document header (DocHeader) has been reorganized to provide a more consistent and intuitive user experience across all backend modules.

This feature includes the following improvements:

Unified language selector 

All backend modules with multi-language support now use a consistent language selector in the top-right area of the DocHeader. This includes:

  • Content > Layout
  • Content > List
  • Content > Preview
  • Record editing (FormEngine)

The language selector displays the currently selected language as the button text (for example "English", "German", and the special All languages) with a descriptive "Language:" prefix for screen readers. This provides immediate visual feedback about the active language while maintaining full accessibility.

The language selector now allows creating new page translations directly from the dropdown (except in Preview mode). The dropdown is organized with existing translations shown first, followed by a divider and a "Create new translation" section header. Languages that do not yet have a translation appear in this separate section.

Selecting a language from the "Create new translation" section will:

  1. Create the page translation via the DataHandler.
  2. Open the FormEngine to edit the newly created translation.
  3. Provide a consistent workflow across all modules.

Unified module actions menu 

Modules with submodules or multiple "actions" now consistently display these options as a dropdown button in the DocHeader button bar. The dropdown button displays the currently active module or action as the button text, providing clear visual context to users.

For accessibility, each dropdown includes a descriptive label:

  • "Display mode" for the Content > Layout module view selector (layout vs language comparison)
  • "Module actions" for general module or submodule navigation
  • Custom labels defined by individual menu configurations

These labels are implemented using visually hidden label elements, ensuring screen readers announce both the dropdown's purpose (for example, "Display mode:") and the current selection (for example, "Layout").

The dropdown is automatically hidden when only a single action is available, reducing visual clutter and showing the dropdown only when navigation choices are actually available.

Examples include Administration > Users with Overview, Users, Groups actions, as well as Content > Status with "Pagetree Overview", "Localization Overview", and more.

This replaces the previous inconsistent mixture of:

  • Back buttons
  • Inline button groups
  • Module menus in different locations

Reorganized DocHeader layout 

The DocHeader now has a more logical structure:

Top row (navigation bar):

  • Left side: Breadcrumb navigation showing the current location
  • Right side: Language selector (when applicable)

Second row (button bar):

  • Left side, group 0: Module actions dropdown (when applicable)
  • Left side, groups 1+: Additional action buttons and context-specific buttons (Save, Close, and so on)
  • Right side: Functional buttons such as "Reload", "Bookmark", or "Clear cache"

Technical details 

Module actions dropdown 

Controllers can use the existing ModuleTemplate::makeDocHeaderModuleMenu() method to automatically create a module actions dropdown based on the module configuration:

$view = $this->moduleTemplateFactory->create($request);
$view->makeDocHeaderModuleMenu();
Copied!

For backward compatibility, the MenuRegistry API is automatically converted to module actions dropdowns.

Language selector integration 

The language selector is integrated via the DocHeaderComponent::setLanguageSelector() method.

First, inject the \TYPO3\CMS\Backend\Template\Components\ComponentFactory into your controller:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    private readonly ComponentFactory $componentFactory,
) {}
Copied!

Then build the language selector dropdown with language items:

// Get available site languages for the current page
$siteLanguages = $this->site->getAvailableLanguages($pageRecord, $backendUser);
$currentLanguageId = (int)$moduleData->get('language');

// Build dropdown button
$languageDropdown = $this->componentFactory->createDropDownButton()
    ->setLabel($languageService->sL('core.core:labels.language'))
    ->setShowLabelText(true)
    ->setShowActiveLabelText(true);

// Create dropdown items for each language
foreach ($siteLanguages as $siteLanguage) {
    $languageId = $siteLanguage->getLanguageId();
    $isActive = $currentLanguageId === $languageId;

    // Build URL for language selection
    $href = $this->uriBuilder->buildUriFromRoute('web_layout', [
        'id' => $pageId,
        'language' => $languageId,
    ]);

    $item = $this->componentFactory->createDropDownRadio()
        ->setHref($href)
        ->setLabel($siteLanguage->getTitle())
        ->setIcon(
            $this->iconFactory->getIcon($siteLanguage->getFlagIdentifier())
        )
        ->setActive($isActive);

    $languageDropdown->addItem($item);
}

$view->getDocHeaderComponent()->setLanguageSelector($languageDropdown);
Copied!

The language selector component is automatically rendered in the top-right area of the DocHeader navigation bar.

Accessibility implementation 

The setShowActiveLabelText(true) method enables display of the active item's label while maintaining accessibility. When enabled with setShowLabelText(true), the button renders the dropdown's label in a visually hidden span followed by the active item's label:

  • Visual users see: "English" (active language)
  • Screen reader users hear: "Language: English" (descriptive label + active language)

When setShowLabelText(false), the accessibility information is provided through aria-label and title attributes instead.

This pattern applies to all dropdown buttons in the DocHeader:

$dropdown->setLabel('Display mode')  // Descriptive label for context
    ->setShowLabelText(true)         // Show label text
    ->setShowActiveLabelText(true);  // Show active item label
Copied!

Impact 

The changes provide a more consistent and intuitive user experience. Users can now create and switch between page translations directly from the DocHeader across all relevant modules, without needing to navigate to specific page areas. The clear separation between existing translations and languages that can be created improves discoverability and reduces confusion about which actions are available.

The language selector and module actions dropdowns now display the currently active selection as the button text, providing immediate visual feedback. Users can see at a glance which language is selected or which module action is active, without needing to open the dropdown.

Accessibility has been significantly improved through the use of descriptive labels rendered as visually hidden elements or aria-label attributes. Screen reader users now receive proper context about each dropdown's purpose (e.g., "Language:", "Display mode:", "Module actions") combined with the current selection, ensuring equal access to navigation functionality.

The module actions dropdown makes it immediately clear which module or action is currently active, and which alternatives are available. The dropdown is only shown when multiple options exist, reducing visual clutter. Removal of redundant back buttons in favor of consistent breadcrumb navigation further reduces visual noise.

Feature: #107889 - Introduce TCA option "itemsProcessors" 

See forge#107889

Description 

As part of centralizing and improving the processing of items for select, check, and radio type fields, a new TCA option itemsProcessors has been introduced as a replacement for itemsProcFunc. This option is an array, allowing any number of processors to be called instead of just one. Processors are ordered by their array key (numerical) and executed in that order. Processors can receive arbitrary data through the $context->processorParameters property.

All processors must implement the ItemsProcessorInterface interface.

Processor methods receive two parameters: a SelectItemCollection instance containing the items, and an ItemsProcessorContext instance providing access to table, field, row data, and configuration. The processor must return a SelectItemCollection. This means that added items can no longer be untyped arrays, making the entire process cleaner and safer.

A new Page TSconfig option is also available, mirroring the existing one for itemsProcFunc. See the example below for syntax.

Example 

The TCA registration might look like this:

EXT:my_extension/Configuration/TCA/my_table.php
 use MyVendor\MyExtension\Processors\SpecialRelationsProcessor;

'relation' => [
    'label' => 'Relational field',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectSingle',
        'items' => [
            [
                'value' => 0,
                'label' => '',
            ],
        ],
        'foreign_table' => 'some_foreign_table',
        'itemsProcessors' => [
            100 => [
                'class' => SpecialRelationsProcessor::class,
                'parameters' => [
                    'foo' => 'bar',
                ],
            ],
            50 => [
                'class' => SpecialRelationsProcessor2::class,
            ],
        ],
    ],
],
Copied!

In this example, SpecialRelationsProcessor2 will be called before SpecialRelationsProcessor.

Here is an example processor:

EXT:my_extension/Classes/Processors/SpecialRelationsProcessor.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Processors;

use TYPO3\CMS\Core\DataHandling\ItemsProcessorContext;
use TYPO3\CMS\Core\DataHandling\ItemsProcessorInterface;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Schema\Struct\SelectItemCollection;

final class SpecialRelationsProcessor implements ItemsProcessorInterface
{
    public function processItems(
        SelectItemCollection $items,
        ItemsProcessorContext $context,
    ): SelectItemCollection {
        $items->add(
            new SelectItem(
                type: 'select',
                label: 'Extra item',
                value: 42,
            )
        );

        return $items;
    }
}
Copied!

The $context->processorParameters property contains any parameters defined in the TCA declaration.

Using Page TSconfig to pass custom parameters to the processor would look like this:

TCEFORM.example_table.content.itemsProcessors.100.foo = bar
Copied!

Note that the numerical key of the processor must be reused. With this setup, the class SpecialRelationsProcessor receives the PHP array ['foo' => 'bar'] in the $context->fieldTSconfig property. The class SpecialRelationsProcessor2 receives an empty array [] (since it is registered with the key 50).

Registration of processors is also possible within FlexForms:

EXT:my_package/Configuration/FlexForms/SomeForm.xml
<some_selector>
    <label>Choice</label>
    <config>
        <type>select</type>
        <renderType>selectSingle</renderType>
        <itemsProcessors>
            <numIndex index="100">
                <class>MyVendor\MyPackage\Processors\SpecialRelationsProcessor</class>
            </numIndex>
        </itemsProcessors>
    </config>
</some_selector>
Copied!

Impact 

It is still possible to use itemsProcFunc, but switching to itemsProcessors is recommended because it offers two main advantages:

  • Being an array, it allows extensions to add processors on top of existing ones and to define their execution order.
  • The processing chain is strictly typed, ensuring safer and more reliable code.

If both itemsProcFunc and itemsProcessors are defined, both are executed, with itemsProcFunc executed first.

Feature: #107953 - Add "Current logged-in users" filter to the "Backend Users" module 

See forge#107953

Description 

A new filter option Current logged-in users has been added to the Administration > Users module. This feature allows administrators to quickly list all backend accounts that are currently active.

The detection mechanism is based on the lastlogin timestamp in combination with the global configuration value $GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] , which defines the maximum lifetime of backend user sessions.

Impact 

Administrators can now filter backend users by those who are currently logged in, providing an immediate overview of active sessions directly within the Administration > Users module.

Feature: #108002 - Introduce built-in symmetric encryption/decryption cipher service 

See forge#108002

Description 

TYPO3 now provides a built-in symmetric encryption and decryption service using the modern XChaCha20-Poly1305 AEAD (Authenticated Encryption with Associated Data) cipher. This service allows extensions and core code to securely encrypt sensitive data such as API tokens, passwords, or personal information without implementing custom encryption solutions or relying on third-party libraries.

Benefits 

The new \TYPO3\CMS\Core\Crypto\Cipher\CipherService provides several advantages:

  • Secure by default: Uses modern, cryptographically secure algorithms provided by the libsodium library (ext-sodium), which is available by default in all PHP versions currently supported by TYPO3.
  • Seamless integration: Secret keys can be derived from TYPO3's existing $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] , eliminating the need to manage additional encryption keys.
  • Authenticated encryption: The AEAD cipher not only encrypts data but also ensures its integrity, preventing tampering.
  • Application-specific keys: Different components can derive isolated encryption keys using seed values, enhancing security through key separation.

Key Derivation 

Secret keys are derived from the existing TYPO3 encryption key using the \TYPO3\CMS\Core\Crypto\Cipher\KeyFactory . This approach leverages the master encryption key that is already configured in your TYPO3 installation, ensuring that different applications or contexts can have their own isolated keys while using the same master key.

Please note that to decrypt data in the future, the encryptionKey must not be changed and must remain available.

Example for deriving a secret key:

use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get the KeyFactory via dependency injection
public function __construct(
    private readonly KeyFactory $keyFactory
) {}

// Derive an application-specific secret key from the TYPO3 encryption key
// The seed 'my-extension-tokens' identifies this specific use case
$secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens');

// You can also derive multiple sub-keys from the same seed
$secretKey1 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 1);
$secretKey2 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 2);
Copied!

Each unique seed produces a different encryption key, allowing for key separation between different features or data types within your extension.

Key Usage/Generation 

Instead of deriving a key, it is also possible to generate a new key from a random value or to use a provided key (e.g., via environment variables).

  • $this->keyFactory->createSharedKeyFromString(getenv('MY_APP_KEY'))
  • $this->keyFactory->generateSharedKey()

Encryption Example 

To encrypt sensitive data, use the CipherService::encrypt() method:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function encryptApiToken(string $apiToken): string
{
    // Derive a secret key for API token encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Encrypt the API token
    $cipherValue = $this->cipherService->encrypt($apiToken, $secretKey);

    // Convert to string for storage in database
    // The result has the format: {base64url-encoded-json}
    // Example: "eyJub25jZSI6IlxcXHUwMDEy4oCmIiwiY2lwaGVyIjoi4oCmIn0"
    // This string contains both the nonce and ciphertext in a JSON structure
    $encryptedString = (string)$cipherValue;

    return $encryptedString;
}
Copied!

Each encryption generates a unique result due to the random nonce, even when encrypting the same plaintext multiple times.

Decryption Example 

To decrypt data, use the CipherService::decrypt() method with the same secret key that was used for encryption:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\CipherValue;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;
use TYPO3\CMS\Core\Crypto\Cipher\CipherDecryptionFailedException;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function decryptApiToken(string $encryptedToken): string
{
    // Derive the same secret key used during encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Parse the cipher text (format: {base64url-encoded-json})
    $cipherValue = CipherValue::fromSerialized($encryptedToken);

    try {
        // Decrypt the token
        $apiToken = $this->cipherService->decrypt($cipherValue, $secretKey);
        return $apiToken;
    } catch (CipherDecryptionFailedException $e) {
        // Decryption failed - wrong key, tampered data, or invalid format
        throw new \RuntimeException(
            'Failed to decrypt API token: ' . $e->getMessage(),
            1762465682,
            $e
        );
    }
}
Copied!

Decryption will fail if the wrong key is used, the data has been tampered with, or the ciphertext format is invalid.

Impact 

The CipherService and KeyFactory are now available as autoconfigured services throughout TYPO3 core and extensions. This provides a standardized, secure way to encrypt and decrypt sensitive data without requiring additional dependencies beyond ext-sodium, which is already a requirement of TYPO3 core.

Extensions can now securely store encrypted data in the database or configuration files using this built-in service, ensuring consistent security practices across the TYPO3 ecosystem.

Feature: #108008 - Automatic reload and shortcut buttons in backend modules 

See forge#108008

Description 

Backend modules now automatically get reload and shortcut buttons added to their document header, ensuring consistent display across all backend modules.

Previously, controllers manually added these buttons with varying group numbers, leading to inconsistent positioning. Now, buttons always appear on the right side, ensuring they are always the last two buttons regardless of what other buttons are added.

Controllers provide shortcut information using the new DocHeaderComponent::setShortcutContext() method:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'site_configuration.edit',
    displayName: sprintf('Edit site: %s', $siteIdentifier),
    arguments: ['site' => $siteIdentifier]
);
Copied!

Buttons are automatically added during rendering before the PSR-14 ModifyButtonBarEvent is dispatched, allowing event listeners to modify or remove them if needed.

Controllers can disable automatic buttons if custom behavior is required:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->disableAutomaticReloadButton();
$view->getDocHeaderComponent()->disableAutomaticShortcutButton();
Copied!

Impact 

Reload and shortcut buttons now appear consistently at the same position across all backend modules, providing a predictable user experience.

Controllers no longer need to manually create these buttons, reducing boilerplate code. Use DocHeaderComponent::setShortcutContext() to provide shortcut information and remove manual button creation, see Deprecation: #108008 - Manual shortcut button creation.

Before:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;

$reloadButton = $this->componentFactory->createReloadButton($uri);
$view->addButtonToButtonBar(
    $reloadButton,
    ButtonBar::BUTTON_POSITION_RIGHT,
    3
);

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// Set shortcut context only
$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Feature: #108016 - Multi-language selection in page and list modules 

See forge#108016

Description 

The Content > Layout and Content > List modules now support selecting multiple languages simultaneously for improved comparison of translated records.

The language selection state is synchronized between both modules, providing a consistent experience when switching between page and list views.

A fallback mechanism is in place for switching between view modes (Layout and Language Comparison) as well as navigating to a page not available in the current language selection.

Impact 

Users can now:

  • Select multiple specific languages to compare
  • Toggle individual languages on / off
  • Quickly select or deselect all available languages
  • See visual feedback with appropriate icons and counts

The language selection is persisted in module data and shared between the page module and list module for the same page.

Feature: #108027 - Type-specific title in TCA types section 

See forge#108027

Description 

It is now possible to define a type-specific title in the TCA types section. This value overrides the global table title defined in ctrl['title'] for the respective record type.

This allows different record types of the same table to display different titles in the TYPO3 backend user interface, making it clearer which kind of record is being created or displayed.

Implementation 

This feature has been implemented in two areas to ensure consistent behavior:

  1. Schema API The TcaSchemaFactory merges type-specific configuration into sub-schemas using array_replace_recursive(). This makes type-specific titles available through $schema->getTitle().
  2. FormEngine The data provider TcaTypesCtrlOverrides merges the type-specific title and previewRenderer into processedTca['ctrl'] during form rendering. This ensures that both FormEngine and any legacy code accessing ctrl directly will see the correct type-specific values.

Example 

EXT:my_extension/Configuration/TCA/tx_my_table.php
return [
    'ctrl' => [
        'title' => 'my_extension.db:tx_my_table',
        'type' => 'record_type',
        // ... other ctrl configuration
    ],
    'types' => [
        'article' => [
            'title' => 'my_extension.db:tx_my_table.type.article',
            'showitem' => 'title, content, author',
        ],
        'news' => [
            'title' => 'my_extension.db:tx_my_table.type.news',
            'showitem' => 'title, content, publish_date',
        ],
        'event' => [
            // No type-specific title - will use the global ctrl['title']
            'showitem' => 'title, content, event_date',
        ],
    ],
    // ... columns configuration
];
Copied!

In this example:

  • The article type displays the title defined in the language file key tx_my_table.type.article.
  • The news type displays the title defined in tx_my_table.type.news.
  • The event type falls back to the global table title from ctrl['title'].

Impact 

Tables with multiple record types can now define more specific and descriptive titles for each type in the backend user interface. This improves usability and clarity for editors by making it immediately obvious which type of record is being created or edited.

This feature is especially useful for:

  • Content element tables such as tt_content with different CType values
  • Tables with distinct record types serving different purposes
  • Plugin records with varying functionality per type
  • Any table where the record type changes the record's purpose or meaning

Feature: #108049 - Modernized translation workflow 

See forge#108049

Description 

A modernized and extensible translation workflow has been introduced in the TYPO3 backend. The new architecture replaces the previous monolithic localization implementation with a step-based wizard system that guides editors through the translation process. Previously available only in the Content > Layout module, the translation wizard is now the default interface for all translation operations across backend modules.

The backend now consistently opens the new translation wizard whenever users initiate a translation operation, providing a unified experience across all modules. The wizard guides users through the translation process in multiple steps, automatically advancing when no user input is required. This streamlined approach ensures that users only interact with steps requiring configuration or confirmation.

Users can choose between two translation modes: Translate (creates a connected translation) and Copy (creates an independent copy in free mode). To maintain consistency, the wizard only offers the translation mode that matches existing translations for a record, preventing mixed modes.

API for extension developers 

The new LocalizationHandlerRegistry provides a flexible architecture for registering and managing different translation strategies. Handlers implement the LocalizationHandlerInterface and are automatically registered via autoconfiguration with the backend.localization.handler tag. This makes the localization system extensible, allowing custom handlers to be added for specialized translation workflows, such as integration with translation services, AI-powered translation, or custom business logic.

The LocalizationFinisherInterface defines how the wizard completes after a successful translation operation. Finishers can be customized by localization handlers to provide the most appropriate completion behavior for their workflow, such as redirecting to a specific page or reloading the current view.

Custom localization handlers 

Extensions can provide custom localization handlers by implementing the LocalizationHandlerInterface :

EXT:my_extension/Classes/Localization/MyCustomHandler.php
namespace MyVendor\MyExtension\Localization;

use TYPO3\CMS\Backend\Localization\LocalizationHandlerInterface;
use TYPO3\CMS\Backend\Localization\LocalizationInstructions;
use TYPO3\CMS\Backend\Localization\LocalizationMode;
use TYPO3\CMS\Backend\Localization\LocalizationResult;

final class MyCustomHandler implements LocalizationHandlerInterface
{
    public function getIdentifier(): string
    {
        return 'my-custom-handler';
    }

    public function getLabel(): string
    {
        return 'my_extension.messages:handler.label';
    }

    public function getDescription(): string
    {
        return 'my_extension.messages:handler.description';
    }

    public function getIconIdentifier(): string
    {
        return 'my-extension-icon';
    }

    public function isAvailable(LocalizationInstructions $instructions): bool
    {
        // Return true if this handler should be available for the given context
        return $instructions->mainRecordType === 'my_table';
    }

    public function processLocalization(
        LocalizationInstructions $instructions
    ): LocalizationResult {
        // Implement custom localization logic
        // Return a LocalizationResult with success status and finisher
    }
}
Copied!

The handler will be automatically registered via autoconfiguration when it implements LocalizationHandlerInterface . No manual service configuration is required.

Custom finishers 

Extensions can create custom finishers by implementing the LocalizationFinisherInterface and providing a corresponding JavaScript module to handle the frontend logic:

EXT:my_extension/Classes/Localization/Finisher/MyCustomFinisher.php
namespace MyVendor\MyExtension\Localization\Finisher;

use TYPO3\CMS\Backend\Localization\Finisher\LocalizationFinisherInterface;

final class MyCustomFinisher implements LocalizationFinisherInterface
{
    public function getType(): string
    {
        return 'my-custom-finisher';
    }

    public function getModule(): string
    {
        return '@vendor/my-extension/localization/finisher/my-custom-finisher.js';
    }

    public function getData(): array
    {
        return [
            'customData' => 'value',
        ];
    }

    public function getLabels(): array
    {
        return [
            'successMessage' => 'my_extension.messages:finisher.success',
        ];
    }

    public function jsonSerialize(): array
    {
        return [
            'type' => $this->getType(),
            'module' => $this->getModule(),
            'data' => $this->getData(),
            'labels' => $this->getLabels(),
        ];
    }
}
Copied!

The corresponding JavaScript module implements the finisher logic. The finisher is executed in the final step of the wizard and can define custom rendering and behavior. Users can skip the finisher step if no interaction is required.

EXT:my_extension/Resources/Public/JavaScript/localization/finisher/my-custom-finisher.js
import LocalizationFinisher from '@typo3/backend/localization/localization-finisher';
import { html, type TemplateResult } from 'lit';

class MyCustomFinisher extends LocalizationFinisher {
  public render(): TemplateResult {
    // Define what is rendered in the finisher step
    // Can return custom UI elements, messages, actions, etc.
    return html`
      <p>${this.finisher.labels.successMessage}</p>
      <button @click=${() => this.execute()}>
        Perform Action
      </button>
    `;
  }

  public async execute(): Promise<void> {
    // Execute finisher logic (e.g., redirect, reload, show notification)
    // This is called when the wizard completes or when the user clicks
    // the custom action button above
    const customData = this.finisher.data.customData;
    console.log('Finisher executing with data:', customData);

    // Perform your custom finisher action here
    // For example: redirect, reload, show notification, etc.
  }
}

export default MyCustomFinisher;
Copied!

Impact 

The new translation workflow provides a consistent and intuitive user experience across all backend modules. Editors benefit from clear step-by-step guidance through the translation process, with the wizard automatically adapting to the context and showing only relevant options.

Extension developers can register custom localization handlers to integrate specialized translation workflows, such as connections to external translation services or AI-powered translation tools.

Feature: #108148 - Alternative Fluid syntax for CDATA sections 

See forge#108148

Description 

A long-standing issue in Fluid templates has been that the Fluid variable and inline ViewHelper syntax collides with inlined CSS or JavaScript code. This issue has now been addressed with Fluid 5: A new alternative syntax has been introduced that makes collisions between CSS/JavaScript and Fluid far less likely.

The normal inline and variable syntax uses single curly braces { } as tokens in Fluid templates. In <![CDATA[ ]]> sections, this syntax is now ignored. Instead, three curly braces {{{ }}} can be used to call Fluid ViewHelpers or to access variables. The tag-based syntax is disabled altogether in CDATA sections.

Example:

<f:variable name="color" value="red" />
<style>
<![CDATA[
    @media (min-width: 1000px) {
        p {
            background-color: {{{color}}};
        }
    }
]]>
</style>
Copied!

The Fluid Explained documentation contains several practical examples of how this new feature can be used: Avoiding syntax collision with JS and CSS.

Note that it's still considered bad practice to put inline CSS or JavaScript code in Fluid templates. Consider using a dedicated API endpoint, data-* attributes or CSS custom properties (also known as CSS variables) to pass dynamic values to JavaScript and CSS.

Impact 

Inline CSS and JavaScript can now be wrapped in CDATA in Fluid templates, which prevents syntax collision with Fluid's inline syntax.

CDATA sections are thus no longer removed from Fluid templates before rendering, see Breaking: #108148 - CDATA Sections No Longer Removed.

Feature: #108148 - Union types for ViewHelpers 

See forge#108148

Description 

Fluid 5 brings support for union types in ViewHelper argument definitions. Previously, it was necessary to specify an argument as mixed if more than one type should be possible. Now it is possible to specify multiple types separated by a pipe character (|).

The following built-in PHP types can also be used:

  • iterable
  • countable
  • callable

Example:

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class MyViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument(
            'file',
            File::class . '|' . FileReference::class,
            'a file object'
        );
        $this->registerArgument(
            'items',
            'iterable',
            'a list of items'
        );
    }
}
Copied!

This feature also applies to the Argument ViewHelper <f:argument>:

<f:argument name="file" type="TYPO3\CMS\Core\Resource\File|TYPO3\CMS\Core\Resource\FileReference" />
Copied!

Note that union types disable automatic type conversion by Fluid, so it might be necessary to specify more types to keep ViewHelpers flexible. Example:

$this->registerArgument(
    'ids',
    'array|string|int',
    'a list of ids, either comma-separated or as array'
);
Copied!

Impact 

Custom ViewHelper implementations have more options to specify an API with strict type requirements and can avoid manual type checks.

Feature: #108166 - Fluid file extension and template resolving 

See forge#108166

Description 

Fluid 5 introduces a specific file extension for template files. The extension is used in combination with a generic extension, which means that existing syntax highlighting in code editors still works.

Before After
MyTemplate.html MyTemplate.fluid.html
MyTemplate.xml MyTemplate.fluid.xml
... ...

As stated in the Fluid release notes, this new file extension is entirely optional. Existing template files will continue to work. A fallback mechanism is in place, which checks for template files in the following order:

templateRootPath: templates/
template: myTemplate
format: html

1. templates/myTemplate.fluid.html
2. templates/myTemplate.html
3. templates/myTemplate
4. templates/MyTemplate.fluid.html
5. templates/MyTemplate.html
6. templates/MyTemplate
Copied!

This means that *.fluid.* files are preferred over files without the new extension if both files exist in the same folder.

Another noteworthy change in Fluid is that template files no longer need to start with an uppercase character: The user-provided spelling of the template name will be tried first, the uppercase variant will be used as a fallback.

Consequences for template overloading 

If multiple template paths are provided (for example if an extension overloads templates or partials of another extension), this new fallback chain for template file names will be executed per template path. In practice this means the following:

  • A TYPO3 community extension can ship *.html files (without the new file extension), which can be overloaded by a sitepackage extension that already uses *.fluid.html files.
  • A TYPO3 community extension can ship *.fluid.html files, which can still be overloaded by a sitepackage extension that uses *.html files.
  • MyTemplate.html will still overload MyTemplate.html like before, and the same applies to *.fluid.html files.

However, since older Fluid versions do not consider *.fluid.* files, it is not supported to use the new file extension in a TYPO3 community extension that still supports TYPO3 versions below 14.

For the TYPO3 Core this means that all template files can safely be renamed to the new file extension because Fluid 5 is always present. To overload templates from the TYPO3 Core, both the new Fluid file extension and the old file extension can be used, which allows TYPO3 community extensions to remain compatible with multiple TYPO3 Core versions.

Edge case: Templates with file extension specified 

Note that the described file extension fallback chain only works if the file extension is not specified explicitly, but rather derived from the template's format. If the file extension is part of the requested template name, Fluid can't reliably add the *.fluid.* file extension automatically and the template needs to be adjusted.

One use case of this would be a template in format json that calls a partial in format html:

MyTemplate.fluid.json
<!-- Won't work if template file is called MyTemplate.fluid.html: -->
<f:render partial="MyTemplate.html" />

<!-- Needs to be adjusted like this: -->
<f:render partial="MyTemplate.fluid.html" />
Copied!

Impact 

While the TYPO3 Core can already switch to the new *.fluid.* file extension, TYPO3 community extensions will probably continue to use *.html for an extended period. However, on a v14 project, the new file extension can already be used, both for individual development and for the integration of TYPO3 community extensions.

Projects and extension authors willing to switch to the new file extension can use the fluid-rename utility extension, which has already been used for the TYPO3 Core.

This new file extension opens new possibilities because it is now easily recognizable which files will be interpreted by Fluid. This will enable better IDE integration and tooling support in the future.

Feature: #108227 - Allow #[IgnoreValidation] and #[Validate] attributes for method parameters 

See forge#108227

Description 

The Extbase attributes #[IgnoreValidation] and #[Validate] can now be used for controller action method parameters.

This extends the current validation behavior of these attributes, where either (1) the complete method or (2) a single parameter is taken into account for validation. While this works fine for (1), using these attributes in context of (2) raises some concerns regarding

  1. duplication of parameter names,
  2. validation of the existence of a configured parameter, and
  3. unnecessary complexity regarding reflection-based handling of these parameters.

History 

When the attributes were originally implemented as Doctrine annotations, the only possible way to implement behavior (2) was to add the appropriate annotation to the method-related annotation.

Since forge#107229, annotations are no longer used in Extbase, and PHP attributes are the only remaining successor in this area. Since PHP attributes support placement at specific method parameters, the existing attributes can safely rewritten to be placed (1) at a specific method or (2) at a specific method parameter.

Usage with method parameters 

The capabilities of the existing attributes are expanded to allow placement at method parameters as well. The previous behavior (defining validation-related behavior at method level using the existing attribute properties) is now deprecated and will be removed with TYPO3 v15 (see deprecation notice).

Impact 

By allowing the usage of both #[IgnoreValidation] and #[Validate] attributes at method parameter level, the previous error-prone behavior is now hardened. In addition, this change improves developer experience and pushes the Extbase ecosystem towards a modernized architecture.

All installations using these attributes on method level need to migrate these as described in the related section of Deprecation #108227 - Migration.

Feature: #108229 - Fluid cache warmup 

See forge#108229

Description 

TYPO3 v14 leverages the revamped Fluid v5 warmup feature and integrates a Fluid template warmup directly into the CLI command typo3 cache:warmup.

The command finds and compiles all *.fluid.* (for example Index.fluid.html) files found in extensions.

Fluid warmup can also be called directly using typo3 fluid:cache:warmup, which will additionally output compile time deprecations found within Fluid template files.

Impact 

The warmup command can be useful to reduce ramp up time after deployments.

Feature: #108240 - Introduce Fresh theme 

See forge#108240

Description 

The new Fresh theme has been introduced for the TYPO3 backend, marking the beginning of broader backend customization capabilities. This theme offers users a modern alternative appearance option with a friendlier purple accent color palette.

Built on the modern CSS architecture, the "Fresh" theme is now the default for new users. It complements the existing Modern and Classic themes. All three themes will be maintained and enhanced equally going forward.

The extensive CSS framework improvements made during the 14.0 cycle now enable easier theme adaptation with minimal code changes, setting the foundation for the expanding theme system.

Theme selection 

Users can select their preferred theme in the User Settings module under the Backend appearance section. The "Fresh" theme provides a more contemporary look and feel while maintaining the familiar TYPO3 backend structure and usability.

All existing themes remain fully supported:

  • Fresh (default for new users) - Modern purple accent colors
  • Modern - Contemporary appearance with traditional accent colors
  • Classic - Traditional TYPO3 backend appearance

Future development 

Additional customization options and modern user interface elements will be incrementally added to the theme system leading up to the long term support release. The modular CSS architecture ensures that all themes benefit from these enhancements equally.

Impact 

The introduction of the "Fresh" theme demonstrates TYPO3's commitment to providing a modern, customizable backend experience. The modular CSS architecture allows for continuous improvement and evolution of the backend design system.

New TYPO3 installations will automatically use the "Fresh" theme, while existing users can opt in through their User Settings. The choice of theme is stored per user, allowing teams with different preferences to work comfortably within the same TYPO3 installation.

Deprecation: #93981 - GraphicalFunctions->gif_or_jpg 

See forge#93981

Description 

Default image formats can now be configured thanks to forge#93981 (see Feature: #93981 - Specify default image conversion processing).

As a result, the method gif_or_jpg() of \TYPO3\CMS\Core\Imaging\GraphicalFunctions is no longer needed.

Fallback behavior for image preview generation now follows the configured file extensions rather than a hardcoded GIF/JPEG switch.

A new method, GraphicalFunctions->determineDefaultProcessingFileExtension(), has been introduced.

It accepts a file extension such as 'pdf' as an argument and returns the corresponding default output extension for preview generation. This method is currently marked as @internal, as it may later be moved to a dedicated service class.

In general, third-party extensions should not determine the output format manually but rely on TYPO3’s built-in image generation APIs.

Deprecated method:

  • \TYPO3\CMS\Core\Imaging\GraphicalFunctions->gif_or_jpg()

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances that directly use the deprecated method.

Migration 

use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$graphicalFunctions = GeneralUtility::makeInstance(GraphicalFunctions::class);

// Before
$filetype = $graphicalFunctions->gif_or_jpg('pdf', 800, 600);
// Returned: 'jpg'

// After
$filetype = $graphicalFunctions->determineDefaultProcessingFileExtension('pdf');
// Returns: 'jpg' (for example, now depends on configuration!)
Copied!

This is a temporary migration using an @internal method, which is subject to change. Code like the above should generally be avoided in third-party extensions.

Instead, use GraphicalFunctions->resize() and specify the argument $targetFileExtension = 'web' so that actual operations use the configured target formats.

Deprecation: #97559 - Deprecate passing an array of configuration values to Extbase attributes 

See forge#97559

Description 

Passing an array of configuration values to Extbase attributes has been deprecated. All configuration values should now be passed as single properties using constructor property promotion. When an array of configuration values is passed for the first available property in an attribute, a deprecation notice will be triggered. The possibility to pass such an array will be removed with TYPO3 v15.

Impact 

The usage of constructor property promotion as an alternative to an array of configuration values enables type safety and value hardening and moves Extbase attributes toward a modern configuration element for models, Data Transfer Objects, and controller actions.

Affected installations 

All installations that make use of Extbase attribute configuration are affected, since this was previously only possible by passing an array of configuration values.

Migration 

Use the available attribute properties instead of an array.

Before: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';

    #[FileUpload([
        'validation' => [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        'uploadFolder' => '1:/user_upload/files/',
    ])]
    protected ?FileReference $bar = null;
}
Copied!

After: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(validator: 'NotEmpty')]
    protected string $foo = '';

    #[FileUpload(
        validation: [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        uploadFolder: '1:/user_upload/files/',
    )]
    protected ?FileReference $bar = null;
}
Copied!

Combined diff: 

 class MyModel extends AbstractEntity
 {
-    #[Validate(['validator' => 'NotEmpty'])]
+    #[Validate(validator: 'NotEmpty')]
     protected string $foo = '';

-    #[FileUpload([
-        'validation' => [
+    #[FileUpload(
+        validation: [
             'required' => true,
             'maxFiles' => 1,
             'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
             'allowedMimeTypes' => ['image/jpeg', 'image/png'],
         ],
-        'uploadFolder' => '1:/user_upload/files/',
-    ])]
+        uploadFolder: '1:/user_upload/files/',
+    )]
     protected ?FileReference $bar = null;
 }
Copied!

Deprecation: #97857 - Deprecate __inheritances operator in form configuration 

See forge#97857

Description 

The custom __inheritances operator, which was available only in YAML configuration files of EXT:form, has been deprecated.

Previously, this operator was used within form definition files to inherit and reuse configuration parts between form element definitions. With native YAML functionality now providing equivalent and more flexible features, this TYPO3-specific operator is no longer necessary.

Developers are encouraged to migrate to standard YAML features such as anchors, aliases, and overrides to avoid code duplication and to simplify form configuration maintenance.

Impact 

Using the __inheritances operator inside a custom YAML form configuration in EXT:form will trigger a PHP E_USER_DEPRECATED error.

Affected installations 

All installations with custom form definitions or form element configurations that use the __inheritances operator in their EXT:form YAML files are affected and need to update those files accordingly.

Migration 

The custom TYPO3 implementation using __inheritances can be replaced with standard YAML syntax.

Developers can achieve the same result by using anchors ( &), aliases ( *), and overrides ( <<:).

Before:

mixins:
  formElementMixins:
    BaseFormElementMixin:
      1761226183:
        identifier: custom
        templateName: Inspector-TextEditor
        label: Custom editor
        propertyPath: custom
    OtherBaseFormElementMixin:
      1761226184:
        identifier: otherCustom
        templateName: Inspector-TextEditor
        label: Other custom editor
        propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            __inheritances:
              10: 'mixins.formElementMixins.BaseFormElementMixin'
              20: 'mixins.formElementMixins.OtherBaseFormElementMixin'
Copied!

After:

customEditor: &customEditor
  1761226183:
    identifier: custom
    templateName: Inspector-TextEditor
    label: Custom editor
    propertyPath: custom

otherCustomEditor: &otherCustomEditor
  identifier: otherCustom
  templateName: Inspector-TextEditor
  label: Other custom editor
  propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            <<: *customEditor
            1761226184: *otherCustomEditor
Copied!

Deprecation: #98453 - Scheduler task registration via SC_OPTIONS 

See forge#98453

Description 

The registration of scheduler tasks via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] has been deprecated in favor of the new native scheduler task feature using TCA.

Previously, scheduler tasks were registered in ext_localconf.php using the following syntax:

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

This approach required a separate AdditionalFieldProviderInterface implementation to handle custom task fields. The AdditionalFieldProvider was responsible for:

  • Rendering form fields in the scheduler module.
  • Validating field input.
  • Saving and loading field values.

The new approach replaces this with native TCA configuration, providing:

  • Better integration with TYPO3's FormEngine.
  • Automatic validation through TCA field configuration.
  • Enhanced security through FormEngine's XSS protection.
  • Consistency with other TYPO3 backend forms.
  • Access to all TCA field types and rendering options.

In addition, the class AbstractAdditionalFieldProvider and the interface AdditionalFieldProviderInterface have been deprecated.

Impact 

Using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] for registering scheduler tasks will stop working in TYPO3 v15.0.

Custom task classes implementing AdditionalFieldProviderInterface should remove this interface implementation. The interface methods ( getAdditionalFields(), validateAdditionalFields(), saveAdditionalFields()) are no longer needed with the new TCA-based approach.

Affected installations 

All installations with custom scheduler tasks registered via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] and using AdditionalFieldProviderInterface .

Migration 

Scheduler tasks should now be registered as native task types using TCA. This provides a more integrated and maintainable approach to task configuration.

Migration steps:

  1. Remove the registration from ext_localconf.php.
  2. Create a TCA override file in Configuration/TCA/Overrides/scheduler_my_task_type.php.
  3. Update your task class to implement the new parameter methods.
  4. Remove the \AdditionalFieldProvider class if it exists.

Example migration 

Before:

ext_localconf.php
use MyVendor\MyExtension\Task\MyTask;
use MyVendor\MyExtension\Task\MyTaskAdditionalFieldProvider;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

After:

Configuration/TCA/Overrides/scheduler_my_task_type.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    // Add custom fields to the tx_scheduler_task table
    ExtensionManagementUtility::addTCAcolumns(
        'tx_scheduler_task',
        [
            'my_extension_field' => [
                'label' => 'my_extension.messages:field.label',
                'config' => [
                    'type' => 'input',
                    'size' => 30,
                    'required' => true,
                    'eval' => 'trim', // FormEngine validation replaces custom validation
                    'placeholder' => 'Enter value here...',
                ],
            ],
            'my_extension_email_list' => [
                'label' => 'my_extension.messages:emailList.label',
                'config' => [
                    'type' => 'text',
                    'rows' => 3,
                    'required' => true, // 'required' validation handled by FormEngine
                    'placeholder' => 'admin@example.com',
                ],
            ],
        ]
    );

    // Register the task type
    ExtensionManagementUtility::addRecordType(
        [
            'label' => 'my_extension.messages:my_task.title',
            'description' => 'my_extension.messages:my_task.description',
            'value' => MyTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'group' => 'my_extension',
        ],
        '
        --div--;core.tabs:general,
            tasktype,
            task_group,
            description,
            my_extension_field,
            my_extension_email_list,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;core.tabs:access,
            disable,
        --div--;core.tabs:extended,',
        [],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Update your (existing) task class to implement the new methods:

EXT:my_extension/Classes/Task/MyTask.php
namespace MyVendor\MyExtension\Task;

use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class MyTask extends AbstractTask
{
    protected string $myField = '';
    protected string $emailList = '';

    public function execute(): bool
    {
        // Your task logic here using $this->myField and $this->emailList
        return true;
    }

    /**
     * Return current field values as an associative array.
     * This method is called during migration from old serialized tasks
     * and when displaying task information.
     */
    public function getTaskParameters(): array
    {
        return [
            'my_extension_field' => $this->myField,
            'my_extension_email_list' => $this->emailList,
        ];
    }

    /**
     * Set field values from an associative array.
     * This method handles both old and new parameter formats for migration.
     *
     * @param array $parameters Values from either old AdditionalFieldProvider or new TCA fields.
     */
    public function setTaskParameters(array $parameters): void
    {
        // Handle migration: check old parameter names first, then new TCA field names
        $this->myField = $parameters['myField'] ?? $parameters['my_extension_field'] ?? '';
        $this->emailList = $parameters['emailList'] ?? $parameters['my_extension_email_list'] ?? '';
    }

    /**
     * Validate task parameters.
     * Only implement this method for validation that cannot be handled by FormEngine.
     * Basic validation like 'required' should be done via TCA 'eval' configuration.
     */
    public function validateTaskParameters(array $parameters): bool
    {
        $isValid = true;

        // Example: Custom email validation (beyond basic 'required' check)
        $emailList = $parameters['my_extension_email_list'] ?? '';
        if (!empty($emailList)) {
            $emails = GeneralUtility::trimExplode(',', $emailList, true);
            foreach ($emails as $email) {
                if (!GeneralUtility::validEmail($email)) {
                    GeneralUtility::makeInstance(FlashMessageService::class)
                        ->getMessageQueueByIdentifier()
                        ->addMessage(
                            GeneralUtility::makeInstance(
                                FlashMessage::class,
                                'Invalid email address: ' . $email,
                                '',
                                ContextualFeedbackSeverity::ERROR
                            )
                        );
                    $isValid = false;
                }
            }
        }

        return $isValid;
    }

    public function getAdditionalInformation(): string
    {
        $info = [];
        if ($this->myField !== '') {
            $info[] = 'Field: ' . $this->myField;
        }
        if ($this->emailList !== '') {
            $info[] = 'Emails: ' . $this->emailList;
        }
        return implode(', ', $info);
    }
}
Copied!

Key methods explained 

The new TCA-based approach uses three key methods for parameter handling:

getTaskParameters(): array

This method is already implemented in AbstractTask to handle task class properties automatically, but it can be overridden in task classes for custom behavior.

The method is primarily used:

  • For migration from the old serialized task format to the new TCA structure.
  • For non-native (deprecated) task types to store their values in the legacy parameters field.

For native TCA tasks, this method is typically no longer needed in custom tasks after the migration has been done, since field values are then stored directly in database columns.

setTaskParameters(array $parameters): void

Sets field values from an associative array. This method handles:

  • Migration from old \AdditionalFieldProvider field names to new TCA field names.
  • Loading saved task configurations when editing or executing tasks.
  • Parameter mapping during task creation and updates.
  • The method should always be implemented, especially for native tasks.

The migration pattern is: $this->myField = $parameters['oldName'] ?? $parameters['new_tca_field_name'] ?? '';

validateTaskParameters(array $parameters): bool

Optional method. Only implement this for validation that cannot be handled by FormEngine.

  • Basic validation (required, trim, etc.) should be done via TCA configuration (required property and eval options).
  • Use this method for complex business logic validation (e.g., email format validation, external API checks).
  • Return false and add a FlashMessage for validation errors.
  • FormEngine automatically handles standard TCA validation rules.

For a complete working example, see SystemStatusUpdateTask and its corresponding TCA configuration in EXT:reports/Configuration/TCA/Overrides/scheduler_system_status_update_task.php.

Deprecation: #106393 - Various methods in BackendUtility 

See forge#106393

Description 

Due to the introduction of the Schema API, several methods of \TYPO3\CMS\Backend\Utility\BackendUtility that retrieve information from $GLOBALS['TCA'] have been deprecated:

  • BackendUtility::getCommonSelectFields()
  • BackendUtility::getItemLabel()
  • BackendUtility::isTableLocalizable()
  • BackendUtility::isTableWorkspaceEnabled()
  • BackendUtility::isRootLevelRestrictionIgnored()
  • BackendUtility::isWebMountRestrictionIgnored()
  • BackendUtility::resolveFileReferences()

Impact 

Calling any of the mentioned methods now triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that directly call these methods are affected.

Migration 

The migration strategy is the same for all cases: use the corresponding Schema API methods directly in your code. In most cases, you'll need to inject TcaSchemaFactory via dependency injection.

getCommonSelectFields 

No substitution is available. The method was marked as @internal already. If your code depends on this functionality, copy the method into your own extension.

getItemLabel 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::getItemLabel('pages', 'title');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
$schema = $this->schemaFactory->has('pages')
    ? $this->schemaFactory->get('pages')
    : null;
return $schema !== null && $schema->hasField('title')
    ? $schema->getField('title')->getLabel()
    : null;
Copied!

isTableLocalizable 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableLocalizable('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Language);
Copied!

isTableWorkspaceEnabled 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableWorkspaceEnabled('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Workspace);
Copied!

isRootLevelRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isRootLevelRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->getCapability(
            TcaSchemaCapability::RestrictionRootLevel
        )->shallIgnoreRootLevelRestriction();
Copied!

isWebMountRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isWebMountRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::RestrictionWebMount);
Copied!

resolveFileReferences 

No substitution is available. Copy the method into your own codebase and adapt it as needed.

Deprecation: #106405 - AbstractTypolinkBuilder->build 

See forge#106405

Description 

The build() method in \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder has been deprecated in favor of the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

When creating custom TypolinkBuilder classes, the traditional approach was to:

  1. Extend \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder
  2. Implement the build() method
  3. Receive dependencies via the constructor

This approach is now deprecated. The new, recommended pattern is to implement TypolinkBuilderInterface directly.

The ContentObjectRenderer property in AbstractTypolinkBuilder has also been deprecated and will be removed in TYPO3 v15.0. It should now be accessed via the \Psr\Http\Message\ServerRequestInterface object instead.

Impact 

Extension authors who have created custom TypolinkBuilder classes that extend AbstractTypolinkBuilder will see deprecation warnings when their link builders are used.

Deprecation warnings occur when:

  • A custom TypolinkBuilder class still implements the build() method
  • Code accesses the deprecated $contentObjectRenderer property
  • Dependencies are passed through the constructor instead of DI

Affected installations 

TYPO3 installations with extensions that:

  • Create custom TypolinkBuilder classes extending AbstractTypolinkBuilder
  • Override or extend the build() method
  • Access the $contentObjectRenderer property directly

Migration 

For end users, link generation continues to work as before using either:

  • ContentObjectRenderer , method typolink()
  • LinkFactory

These public APIs are not affected and do not trigger deprecations.

For extension developers, custom TypolinkBuilder implementations should now use the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

  1. Implement the new interface:
Recommended migration approach
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

// Before (deprecated)
class MyCustomLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(
        array &$linkDetails,
        string $linkText,
        string $target,
        array $conf
    ): LinkResultInterface {
        // Custom logic using $this->contentObjectRenderer
    }
}

// After (recommended)
class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = ''
    ): LinkResultInterface {
        $contentObjectRenderer = $request->getAttribute(
            'currentContentObject'
        );
        // Custom logic using $contentObjectRenderer
    }
}
Copied!
  1. Use dependency injection for required services instead of accessing them through global state or constructor arguments.
  2. Retrieve ContentObjectRenderer from the request object rather than the deprecated property.

All implementations of TypolinkBuilderInterface are automatically registered as public services in the Dependency Injection container, so no manual service configuration is necessary.

Extensions can maintain backward compatibility during the transition by implementing both the old build() and the new buildLink() methods.

Deprecation: #106527 - markFieldAsChanged() moved to FormEngine main module 

See forge#106527

Description 

The static method markFieldAsChanged() in the module @typo3/backend/form-engine-validation is used to modify the markup in the DOM to mark a field as changed. Technically, this is unrelated to validation itself, therefore the method has been moved to @typo3/backend/form-engine.

Impact 

Calling markFieldAsChanged() from @typo3/backend/form-engine-validation will trigger a deprecation notice in the browser console.

Affected installations 

All extensions using the deprecated code are affected.

Migration 

If not already done, import the main FormEngine module and call markFieldAsChanged() from there.

Example:

- import FormEngineValidation from '@typo3/backend/form-engine-validation.js';
+ import FormEngine from '@typo3/backend/form-engine.js';

- FormEngineValidation.markFieldAsChanged(fieldReference);
+ FormEngine.markFieldAsChanged(fieldReference);
Copied!

Example compatibility layer:

import FormEngine from '@typo3/backend/form-engine.js';
import FormEngineValidation from '@typo3/backend/form-engine-validation.js';

if (typeof FormEngine.markFieldAsChanged === 'function') {
  FormEngine.markFieldAsChanged(fieldReference);
} else {
  FormEngineValidation.markFieldAsChanged(fieldReference);
}
Copied!

Deprecation: #106618 - GeneralUtility::resolveBackPath 

See forge#106618

Description 

The method \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath has been marked as deprecated and will be removed in TYPO3 v15.0.

It served as a mechanism to remove relative path segments such as ".." when referencing files or directories. This was particularly important before TYPO3 v7, when every TYPO3 backend module had its own route and entry point PHP file. Nowadays, it is a relic from the past.

Since TYPO3 v13, this method has become even less relevant, as both the TYPO3 backend and frontend now share the same main entry point file (index.php).

Impact 

TYPO3 no longer resolves the back path of resource references or normalizes paths when rendering or referencing resources in the HTML output - neither in the frontend nor in the backend.

However, existing references will continue to work.

Affected installations 

TYPO3 installations with custom TypoScript inclusions or backend modules that reference files using relative paths may be affected. Such usage is uncommon in modern TYPO3 installations.

Migration 

References to resources should now use the EXT: prefix or be written relative to the public web path of the TYPO3 installation.

References to JavaScript modules (ES6 modules) should be managed through import maps using module names instead of relative paths.

Deprecation: #106821 - Workspace aware inline child tables are enforced 

See forge#106821

Description 

TCA tables that are used as inline child tables in a standard foreign_table relationship must be declared as workspace aware if their parent table is workspace aware.

Impact 

The TYPO3 Core may automatically add versioningWS = true to the ctrl section of inline child tables. In this case, a deprecation-level log entry will be issued, indicating that the TCA definition should be updated accordingly.

This TCA definition should be added even if the workspace system extension is not loaded. The database schema analyzer will then automatically add the required workspace-related database columns.

Affected installations 

TYPO3 instances with extensions that define inline relations where the parent table is workspace aware but the child table is not are affected.

A typical scenario involves inline child tables attached to the tt_content table.

Migration 

An automatic TCA migration scans TCA configurations for this scenario and adds versioningWS = true to affected child tables. Developers should add this declaration manually to their TCA to satisfy the migration and suppress deprecation log messages.

'ctrl' => [
    'versioningWS' => true,
],
Copied!

Note that the automatic migration does not detect children attached to inline fields within type => 'flex' (flex form) fields. Developers and integrators should still explicitly declare such child tables as workspace aware.

In general, the combination of "parent is workspace aware" and "child is not workspace aware" is not supported by the TYPO3 Core in inline foreign_table setups—regardless of whether the parent field is a database column or part of a flex form.

Deprecation: #106969 - Deprecate User TSConfig auth.BE.redirectToURL 

See forge#106969

Description 

The User TSConfig option auth.BE.redirectToURL has been marked as deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated User TSConfig option triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the User TSConfig option auth.BE.redirectToURL are affected.

Migration 

If a redirect after a successful backend login is required, create a custom PSR-15 middleware to handle the redirection.

Deprecation: #107047 - ExtensionManagementUtility::addPiFlexFormValue() 

See forge#107047

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() has been deprecated.

This method was historically used to assign FlexForm definitions to plugins registered as subtypes in the list_type field of tt_content. With the removal of subtypes (see Breaking: #105377 - Deprecated functionality removed) and the shift toward registering plugins as dedicated record types via CType, as well as the removal of the ds_pointerField and multi-entry ds array format (see Breaking: #107047 - Remove pointer field functionality of TCA flex), this separate method call is no longer necessary.

Impact 

Calling this method will trigger a deprecation warning. The extension scanner will also report any usages. The method will be removed in TYPO3 v15.0.

Affected installations 

Extensions that call \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() to assign FlexForm definitions to plugins or content elements are affected.

Migration 

Instead of using this method, define the FlexForm configuration using one of the following approaches:

Option 1: Register via registerPlugin() 

Provide the FlexForm definition directly when registering the plugin. This has been possible since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'My Plugin Title',
    'my-extension-icon',
    'plugins',
    'Plugin description',
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 2: Register via addPlugin() for non-Extbase plugins 

This is also supported since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\SelectItemUtility\SelectItem;

ExtensionManagementUtility::addPlugin(
    new SelectItem(
        'select',
        'My Plugin Title',
        'myextension_myplugin',
        'my-extension-icon',
        'plugins',
        'Plugin description'
    ),
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 3: Define FlexForm via TCA columnsOverrides 

You can also directly define the FlexForm in TCA:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['types']['my_plugin']['columnsOverrides']
    ['pi_flexform']['config']['ds'] =
    'FILE:EXT:my_extension/Configuration/FlexForm.xml';
Copied!

Deprecation: #107057 - Deprecate auto-render assets sections 

See forge#107057

Description 

The auto-rendering of template sections HeaderAssets and FooterAssets available in Fluid templates has been marked as deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated sections will raise a deprecation-level log error and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the sections HeaderAssets and FooterAssets in Fluid templates.

Migration 

It is recommended to use the f:asset.script or f:asset.css ViewHelpers from the TYPO3 Asset Collector API to render required assets.

In scenarios where the f:asset.script or f:asset.css ViewHelpers are not suitable, users can use the f:page.headerData or f:page.footerData ViewHelpers to render custom HTML header or footer markup.

Deprecation: #107225 - Boolean sort direction in FileList->start() 

See forge#107225

Description 

The fourth parameter of the method \TYPO3\CMS\Filelist\FileList::start() has been renamed from $sortRev to $sortDirection and now accepts both boolean values (for backward compatibility) and SortDirection enum values.

Passing a boolean value for the sort direction (fourth parameter of FileList::start()) has been deprecated in favor of the new \TYPO3\CMS\Filelist\Type\SortDirection enum, providing better type safety and clarity. The parameter name has also changed from $sortRev to $sortDirection to more accurately describe its purpose.

Impact 

Calling FileList::start() with a boolean value as the fourth parameter triggers a deprecation warning. The functionality will continue to work for now but will be removed in TYPO3 v15.0.

Affected installations 

All installations using FileList::start() directly with a boolean value for the sort direction parameter are affected. This mainly applies to custom file browser implementations or extensions that instantiate and configure the FileList class directly, even though it is marked as @internal.

Migration 

Replace boolean values with the corresponding SortDirection enum values:

Example migration
use TYPO3\CMS\Filelist\Type\SortDirection;

// Before (deprecated)
$fileList->start($folder, $currentPage, $sortField, false, $mode);
$fileList->start($folder, $currentPage, $sortField, true, $mode);

// After
$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::ASCENDING,
    $mode
);

$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::DESCENDING,
    $mode
);
Copied!

The migration maintains the same functionality:

  • true (descending) → SortDirection::DESCENDING
  • false (ascending) → SortDirection::ASCENDING

Deprecation: #107229 - Deprecate Annotation namespace of Extbase attributes 

See forge#107229

Description 

With the removed support for PHP annotations in Extbase, the class namespace \TYPO3\CMS\Extbase\Annotation no longer reflects classes using PHP annotations. It now contains PHP attributes only.

The class namespace of all PHP attributes used with models, DTOs, and controller actions in the Extbase context has been moved to \TYPO3\CMS\Extbase\Attribute.

Developers and integrators are advised to migrate existing class usages to the new namespace. The deprecated namespace will be removed in TYPO3 v15.0.

Impact 

Usage of the deprecated class namespace is no longer recommended but will continue to work until TYPO3 v15.0.

Affected installations 

Instances using the deprecated class namespace for Extbase attributes in models, DTOs, or controller actions are affected.

Migration 

Aside from migrating PHP annotations to PHP attributes, existing usages of the class namespace \TYPO3\CMS\Extbase\Annotation should be updated to \TYPO3\CMS\Extbase\Attribute:

-TYPO3\CMS\Extbase\Annotation
+TYPO3\CMS\Extbase\Attribute
Copied!

Before: 

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

After: 

use TYPO3\CMS\Extbase\Attribute as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

Deprecation: #107287 - FileCollectionRegistry->addTypeToTCA() method 

See forge#107287

Description 

The method \TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry->addTypeToTCA() has been deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

This method was originally intended to register additional file collection types by directly manipulating the global $GLOBALS['TCA'] array for the sys_file_collection table. With modern TCA configuration patterns, this approach is no longer recommended.

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances using the FileCollectionRegistry->addTypeToTCA() method directly are affected.

The extension scanner will report usages as weak match.

Migration 

Instead of using this method, configure file collection types directly in your TCA configuration files. Move the TCA configuration from the method call to your extension's Configuration/TCA/Overrides/sys_file_collection.php file.

Before (deprecated)
use TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$fileCollectionRegistry = GeneralUtility::makeInstance(
    FileCollectionRegistry::class
);
$fileCollectionRegistry->addTypeToTCA(
    'mytype',
    'My Collection Type',
    'description,my_field',
    ['my_field' => ['config' => ['type' => 'input']]]
);
Copied!
After (recommended) – EXT:my_extension/Configuration/TCA/Overrides/sys_file_collection.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

$GLOBALS['TCA']['sys_file_collection']['types']['mytype'] = [
    'showitem' => '
        sys_language_uid, l10n_parent, l10n_diffsource,
        title, --palette--;;1, type, description, my_field
    ',
];

$GLOBALS['TCA']['sys_file_collection']['columns']['type']['config']['items'][] =
    [
        'label' => 'My Collection Type',
        'value' => 'mytype',
    ];

// Add additional columns if needed
ExtensionManagementUtility::addTCAcolumns(
    'sys_file_collection',
    [
        'my_field' => [
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);
Copied!

Deprecation: #107413 - PathUtility getRelativePath(to) methods 

See forge#107413

Description 

The following methods in \TYPO3\CMS\Core\Utility\PathUtility have been marked as deprecated and will be removed in TYPO3 v15.0:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

These methods are no longer needed, as TYPO3's path handling has been simplified due to the unification of the entry point URLs.

Since TYPO3 v13, both frontend and backend use the same main entry point (htdocs/index.php), making relative path calculations obsolete.

Impact 

Calling these methods will trigger a PHP deprecation warning. They will continue to work as before until they are removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call the deprecated methods are affected:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

The extension scanner will report any usage as a strong match.

Migration 

Instead of calculating relative paths manually, use absolute paths or the appropriate TYPO3 APIs for path handling:

  • Use GeneralUtility::getFileAbsFileName() for extension resources
  • Use the EXT: prefix when referencing extension resources
  • Use PathUtility::getPublicResourceWebPath() for public extension resources
  • Reference paths relative to the public web path of the TYPO3 installation
Before (deprecated)
use TYPO3\CMS\Core\Utility\PathUtility;

$relativePath = PathUtility::getRelativePath($sourcePath, $targetPath);
$relativeToPath = PathUtility::getRelativePathTo($absolutePath);
Copied!
After (recommended)
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

// Use absolute paths or appropriate TYPO3 APIs
$absolutePath = GeneralUtility::getFileAbsFileName(
    'EXT:my_extension/Resources/Public/file.js'
);

$webPath = PathUtility::getPublicResourceWebPath(
    'EXT:my_extension/Resources/Public/file.js'
);
Copied!

Deprecation: #107436 - Localization Parsers 

See forge#107436

Description 

Due to the migration to Symfony Translation components (see Feature: #107436 - Symfony Translation Component integration), the following localization parser classes have been deprecated:

  • \TYPO3\CMS\Core\Localization\Parser\AbstractXmlParser
  • \TYPO3\CMS\Core\Localization\Parser\LocalizationParserInterface
  • \TYPO3\CMS\Core\Localization\Parser\XliffParser

The global configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] has been removed and replaced with $GLOBALS['TYPO3_CONF_VARS']['LANG']['loader'] .

Impact 

Using any of the mentioned parser classes will raise a deprecation level log entry and will stop working in TYPO3 v15.0.

The extension scanner will report usages as strong match.

Affected installations 

Instances using the deprecated localization parser classes directly or configuring custom parsers via the removed global configuration option.

Migration 

Replace usage of the deprecated parser classes with Symfony Translation loaders. Use the new XliffLoader class for XLIFF file processing.

The Symfony Translator and its loaders are now responsible for file parsing and should be used instead of the deprecated TYPO3 parsers.

For custom localization needs, implement Symfony Translation loader interfaces instead of the deprecated TYPO3 parser interfaces.

Configuration changes:

use TYPO3\CMS\Core\Localization\Loader\XliffLoader;
use TYPO3\CMS\Core\Localization\Parser\XliffParser;

// Before
$GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser']['xlf'] = XliffParser::class;

// After
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['xlf'] = XliffLoader::class;
Copied!

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Deprecation: #107537 - GeneralUtility::createVersionNumberedFilename 

See forge#107537

Description 

GeneralUtility::createVersionNumberedFilename adds cache busting to a file URL, when called in a certain and correct order with other legacy API methods to create URLs from system resources.

This class and its functionality is superseded by the System Resource API.

Impact 

Calling this method will trigger a PHP deprecation warning. The method will continue to work as is, until it is removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call this deprecated method:

  • GeneralUtility::createVersionNumberedFilename

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $file): string
{
    $file = GeneralUtility::getFileAbsFileName($file);
    $partialUrl = GeneralUtility::createVersionNumberedFilename($file);
    return PathUtility::getAbsoluteWebPath($partialUrl);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - FilePathSanitizer service 

See forge#107537

Description 

\TYPO3\CMS\Frontend\Resource\FilePathSanitizer was introduced as a service class to replace the logic that was previously part of $TSFE->tmpl.

The goal of this class was to validate and manipulate given strings to be used for URL generation later in the process.

This class and its functionality are superseded by the System Resource API.

Impact 

All installations using the FilePathSanitizer will trigger a deprecation notice when the class is instantiated.

Otherwise, this class will continue to work as is, until removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that instantiate FilePathSanitizer .

The extension scanner will report any usage as a strong match.

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly FilePathSanitizer $pathSanitizer,
) {}

public function renderUrl(string $someString): string
{
    $pathRelativeToPublicDir = $this->pathSanitizer->sanitize($someString);
    return $this->codeThatDoesDetectTheCorrectUrlPrefix()
        . $pathRelativeToPublicDir;
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - TYPO3\CMS\Core\Utility\PathUtility::getPublicResourceWebPath 

See forge#107537

Description 

The static method \TYPO3\CMS\Core\Utility\PathUtility::getPublicResourceWebPath($extResource) was marked internal since its introduction. Since there were no good alternatives to this API, it is now deprecated first, before being removed with TYPO3 v15.0.

Impact and affected installations 

TYPO3 installations using PathUtility::getPublicResourceWebPath($extResource) will receive a deprecation message for each call of this method.

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $extResource): string
{
    return PathUtility::getPublicResourceWebPath($extResource);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107550 - Table Garbage Collection Task configuration via $GLOBALS 

See forge#107550

Description 

The \TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']

has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the TableGarbageCollectionTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

The extension scanner will report any usage as a weak match.

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']
    [TableGarbageCollectionTask::class]['options']['tables']
    ['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'expirePeriod' => 90,
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_table_garbage_collection.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'expirePeriod' => 90,
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the expirePeriod of table sys_log:

use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['sys_log']['expirePeriod'] = 240;
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107562 - IP Anonymization Task configuration via $GLOBALS 

See forge#107562

Description 

The \TYPO3\CMS\Scheduler\Task\IpAnonymizationTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'] has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the IpAnonymizationTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

The extension scanner will report any usage as a weak match.

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables']['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'ipField' => 'private_ip',
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_ip_anonymization.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables'] ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'ipField' => 'private_ip',
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the dateField of sys_log:

use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables']['sys_log']['dateField']
        = 'custom_date';
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107648 - InfoboxViewHelper STATE_* constants 

See forge#107648

Description 

The public constants in \TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper for defining the state or severity of an infobox have been deprecated:

  • InfoboxViewHelper::STATE_NOTICE
  • InfoboxViewHelper::STATE_INFO
  • InfoboxViewHelper::STATE_OK
  • InfoboxViewHelper::STATE_WARNING
  • InfoboxViewHelper::STATE_ERROR

These constants have been superseded by the dedicated enum \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity , which provides a single source of truth for severity levels across the entire TYPO3 Core and improves type safety and maintainability.

Impact 

Using these constants will trigger a PHP deprecation warning. The constants will be removed in TYPO3 v15.0. The extension scanner will report usages as weak match.

Affected installations 

Instances using any of the STATE_* constants from InfoboxViewHelper in their PHP code or Fluid templates.

Migration 

Replace the deprecated constants with the corresponding ContextualFeedbackSeverity enum.

Example (PHP)
// Before
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
$state = InfoboxViewHelper::STATE_ERROR;

// After - Recommended: Use the enum directly
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
$severity = ContextualFeedbackSeverity::ERROR;

// Alternative: Use the integer value when explicitly needed
$stateValue = ContextualFeedbackSeverity::ERROR->value;
Copied!

In Fluid templates, use the enum via f:constant():

Example (Fluid Template)
<!-- Before -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper::STATE_ERROR')}">
    Error message
</f:be.infobox>

<!-- After -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR')}">
    Error message
</f:be.infobox>
Copied!

The InfoboxViewHelper has been updated to accept both the enum directly and integer values for backward compatibility.

Mapping table 

Deprecated constant Replacement Value
InfoboxViewHelper::STATE_NOTICE ContextualFeedbackSeverity::NOTICE->value -2
InfoboxViewHelper::STATE_INFO ContextualFeedbackSeverity::INFO->value -1
InfoboxViewHelper::STATE_OK ContextualFeedbackSeverity::OK->value 0
InfoboxViewHelper::STATE_WARNING ContextualFeedbackSeverity::WARNING->value 1
InfoboxViewHelper::STATE_ERROR ContextualFeedbackSeverity::ERROR->value 2

Deprecation: #107725 - Deprecate usage of array in password for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password.

With this change, TYPO3's Redis cache backend supports username and password authentication directly. You can now configure the TYPO3 Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The password configuration option of the Redis cache backend is now typed as array|string.

Setting this configuration option with an array is deprecated and will be removed in TYPO3 v15.0.

Affected installations 

All installations using the Redis cache backend and configuring the password option as an array containing both username and password values are affected.

Migration 

Use the dedicated configuration options username and password instead of passing an array to password.

Before (deprecated):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'password' => [
            'user' => 'redis',
            'pass' => 'redis',
        ],
    ];
Copied!

After (recommended):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Deprecation: #107813 - Deprecate MetaInformation API in DocHeader 

See forge#107813

Description 

The \TYPO3\CMS\Backend\Template\Components\MetaInformation class and related methods in \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent have been deprecated in favor of the new breadcrumb component architecture.

The following have been marked as deprecated:

  • DocHeaderComponent::setMetaInformation()
  • DocHeaderComponent::setMetaInformationForResource()
  • MetaInformation class

These APIs were previously used to display page navigation paths in the backend document header. This functionality is now handled by the breadcrumb component, which provides richer context and better navigation capabilities.

Impact 

Calling any of the deprecated methods will trigger a PHP E_USER_DEPRECATED error.

The MetaInformation class is now marked as @internal and should not be used in extensions.

Affected installations 

All installations using custom backend modules that call:

  • $view->getDocHeaderComponent()->setMetaInformation($pageRecord)
  • $view->getDocHeaderComponent()->setMetaInformationForResource($resource)

or any custom code relying on the MetaInformation class.

The Extension Scanner will detect usage of the deprecated methods and classes, making it easy to identify code that needs to be updated.

Migration 

Replace calls to the deprecated methods with the new convenience methods on DocHeaderComponent .

Before:

Example (before)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setMetaInformation($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setMetaInformationForResource($resource);
Copied!

After:

Example (after)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setResourceBreadcrumb($resource);
Copied!

An additional convenience method is available for records:

Example (record breadcrumb)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For any record type (by table and UID)
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);
Copied!

For advanced scenarios requiring custom breadcrumb logic (such as conditional context selection based on controller state), see the implementation in \TYPO3\CMS\Backend\Controller\EditDocumentController , which uses \TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory directly with setBreadcrumbContext().

Deprecation: #107823 - ButtonBar, Menu, and MenuRegistry make* methods deprecated 

See forge#107823

Description 

The factory methods in \TYPO3\CMS\Backend\Template\Components\ButtonBar for creating button instances, in \TYPO3\CMS\Backend\Template\Components\Menu for creating menu item instances, and in \TYPO3\CMS\Backend\Template\Components\MenuRegistry for creating menu instances have been deprecated in favor of using the new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class directly.

The following methods are now deprecated:

  • ButtonBar::makeGenericButton()
  • ButtonBar::makeInputButton()
  • ButtonBar::makeSplitButton()
  • ButtonBar::makeDropDownButton()
  • ButtonBar::makeLinkButton()
  • ButtonBar::makeFullyRenderedButton()
  • ButtonBar::makeShortcutButton()
  • ButtonBar::makeButton()
  • Menu::makeMenuItem()
  • MenuRegistry::makeMenu()

Impact 

Calling any of the deprecated make*() methods on ButtonBar , \Menu, or MenuRegistry will trigger a PHP deprecation notice.

The methods continue to work in TYPO3 v14 but will be removed in TYPO3 v15.

Affected installations 

All extensions using ButtonBar::make*() methods to create buttons, Menu::makeMenuItem() to create menu items, or MenuRegistry::makeMenu() to create menus are affected. The extension scanner will report any usages.

Migration 

Inject \TYPO3\CMS\Backend\Template\Components\ComponentFactory in your controller and use its create*() methods instead of ButtonBar::make*().

Before:

Example (before)
use Psr\Http\Message\ResponseInterface;

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $buttonBar->makeLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

After:

Example (after)
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $this->componentFactory->createLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

Additionally, consider using the preconfigured button creation methods like createBackButton(), createCloseButton(), createSaveButton(), createReloadButton(), and createViewButton() for common button patterns.

For the low-level makeButton(string $className) method, use GeneralUtility::makeInstance() directly or the appropriate ComponentFactory::create*() method:

Example (button instantiation)
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before:
$button = $buttonBar->makeButton(MyCustomButton::class);

// After (option 1 - direct instantiation):
$button = GeneralUtility::makeInstance(MyCustomButton::class);

// After (option 2 - via factory if it's a standard button):
$button = $this->componentFactory->createLinkButton();
Copied!

For Menu::makeMenuItem(), use ComponentFactory::createMenuItem():

Example (menu items)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menu = $menuRegistry->makeMenu();
$menuItem = $menu->makeMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menu = $this->componentFactory->createMenu();
$menuItem = $this->componentFactory->createMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);
Copied!

For MenuRegistry::makeMenu(), use ComponentFactory::createMenu():

Example (menus)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $menuRegistry->makeMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $this->componentFactory->createMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');
Copied!

Additionally, note that Menu::addMenuItem() now returns static to support fluent interface patterns:

Example (fluent chaining)
// Now possible with fluent interface:
$menu->addMenuItem($menuItem1)
    ->addMenuItem($menuItem2)
    ->addMenuItem($menuItem3);
Copied!

Deprecation: #107938 - Deprecate unused XLIFF files 

See forge#107938

Description 

The following XLIFF files have been deprecated, as they are not used in the TYPO3 Core anymore:

  • EXT:backend/Resources/Private/Language/locallang_view_help.xlf
  • EXT:backend/Resources/Private/Language/locallang_sitesettings_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_mod.xlf
  • EXT:belog/Resources/Private/Language/locallang_mod.xlf
  • EXT:beuser/Resources/Private/Language/locallang_mod.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_usertools.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_system.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_site.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_help.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_admintools.xlf
  • EXT:core/Resources/Private/Language/locallang_tsfe.xlf
  • EXT:dashboard/Resources/Private/Language/locallang_mod.xlf
  • EXT:extensionmanager/Resources/Private/Language/locallang_mod.xlf
  • EXT:form/Resources/Private/Language/locallang_module.xlf
  • EXT:indexed_search/Resources/Private/Language/locallang_mod.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallUpgrade.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallSettings.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallMaintenance.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallEnvironment.xlf
  • EXT:install/Resources/Private/Language/BackendModule.xlf
  • EXT:info/Resources/Private/Language/locallang_mod_web_info.xlf
  • EXT:linkvalidator/Resources/Private/Language/Module/locallang_mod.xlf
  • EXT:recycler/Resources/Private/Language/locallang_mod.xlf

They will be removed with TYPO3 v15.0.

The console command vendor/bin/typo3 language:domain:list does not list deprecated language domains, unless the option --deprecated is used.

Impact 

Using a label reference from one of these files triggers a E_USER_DEPRECATED error.

Affected installations 

Third-party extensions and site packages that use labels from the listed sources will not be able to display the affected labels with TYPO3 v15.0.

Migration 

If the desired string is contained in another language domain, consider using that domain. Otherwise, move the required labels into your extension or site package.

Deprecation: #107963 - sys_redirect default type name changed to "default" 

See forge#107963

Description 

The default type name for the sys_redirect table has been changed from '1' to 'default' in TYPO3 v14.0 to align with TYPO3 naming conventions and allow for better type extensibility.

Extensions that directly access $GLOBALS['TCA']['sys_redirect']['types']['1'] to manipulate the redirect TCA configuration must be updated to use the new 'default' key instead.

Impact 

Direct access to $GLOBALS['TCA']['sys_redirect']['types']['1'] will no longer work, as the type has been renamed to 'default'.

Extensions that manipulate the sys_redirect TCA type configuration must be updated accordingly.

The TCA migration layer will automatically migrate any custom $GLOBALS['TCA']['sys_redirect']['types']['1'] definitions to 'default' during TCA compilation, but a deprecation message will be logged.

Affected installations 

Instances with extensions that directly manipulate $GLOBALS['TCA']['sys_redirect']['types']['1'] to customize the default redirect record type.

Migration 

Update your TCA override files to use the new type name 'default' instead of '1'.

// Before - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['1']['label']
    = 'My custom label';

// After - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['default']['label']
    = 'My custom label';
Copied!

Deprecation: #108008 - Manual shortcut button creation 

See forge#108008

Description 

Manually creating and adding \ShortcutButton instances to the button bar is deprecated and will trigger a deprecation warning.

Controllers should use the new \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent::setShortcutContext() method instead, which automatically creates and positions the shortcut button.

Impact 

Controllers that manually create and add \ShortcutButton instances to the button bar will trigger a deprecation warning. The shortcut button will still work as expected.

Affected installations 

Installations with custom backend modules that manually create shortcut buttons.

Migration 

Replace manual shortcut button creation with the new API.

Before:

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Deprecation: #108227 - Usage of #[IgnoreValidation] and #[Validate] attributes for parameters at method level 

See forge#108227

Description 

Usage of the following extbase attribute properties is deprecated since TYPO3 v14.0:

  • $argumentName property of #[IgnoreValidation] attribute
  • $param property of #[Validate] attribute

Instead of using these properties, the containing attributes should be placed directly at the appropriate method parameters.

Example:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Impact 

Passing a value other than null to the mentioned attribute parameters will trigger a deprecation warning. Validation will still work as expected, but will stop working with TYPO3 v15.

Affected installations 

All installations using the #[IgnoreValidation] and #[Validate] attributes in Extbase context in combination with the mentioned attribute parameters are affected.

Migration 

Developers can easily migrate their implementations by moving parameter-related attributes next to the method parameters instead of the related method.

Before:

final class FooController extends ActionController
{
    #[IgnoreValidation(argumentName: 'something')]
    public function barAction(string $something): ResponseInterface
    {
        // Do something...
    }

    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
    public function bazAction(string $anythingNotEmpty): ResponseInterface
    {
        // Do something...
    }
}
Copied!

After:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Combined diff:

 final class FooController extends ActionController
 {
-    #[IgnoreValidation(argumentName: 'something')]
-    public function barAction(string $something): ResponseInterface
-    {
+    public function barAction(
+        #[IgnoreValidation]
+        string $something,
+    ): ResponseInterface {
         // Do something...
     }


-    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
-    public function bazAction(string $anythingNotEmpty): ResponseInterface
-    {
+    public function bazAction(
+        #[Validate(validator: 'NotEmpty')]
+        string $anythingNotEmpty,
+    ): ResponseInterface {
         // Do something...
     }
 }
Copied!

Important: #104027 - New ViewHelper argument "module" to define module context 

See forge#104027

Description 

A new optional argument module has been added to the following ViewHelpers:

  • <be:link.editRecord>
  • <be:link.newRecord>
  • <be:uri.editRecord>
  • <be:uri.newRecord>

The module argument allows integrators to explicitly define the backend module context used when opening the FormEngine to edit or create a record. When set, this module will be highlighted as active in the backend menu, providing better navigation context.

This is particularly useful in scenarios where the default context cannot be reliably inferred.

Usage Example 

Example usage of module argument
<be:link.editRecord table="tt_content" uid="{record.uid}" module="web_layout">
    Edit this content element
</be:link.editRecord>

<be:uri.newRecord table="custom_table" pid="123" module="web_list" />
Copied!

Impact 

When used, the module argument ensures a more accurate and predictable backend editing experience by controlling which module is marked as active when the FormEngine opens.

Important: #105244 - Updated default .htaccess template 

See forge#105244

Description 

When installing TYPO3 for the first time, a .htaccess file is added to the htdocs or public directory when running TYPO3 via an Apache web server.

In addition to several TYPO3-specific optimizations, this file mainly contains rules (using the "mod_rewrite" Apache module) that redirect all URL requests for non-existent files within a TYPO3 project to the main index.php entry point file.

For new installations, this file now contains updated configuration that can also be applied to existing TYPO3 setups to reflect the current default behavior.

Key changes:

  • URL requests within /_assets/ and /fileadmin/ are no longer redirected, as these directories contain resources either managed by TYPO3 or by editors.
  • The directory /_assets/ has been included since TYPO3 v12 in Composer-based installations and is now officially added.
  • The folder /uploads/ is no longer maintained by TYPO3 since v11 and is now removed from the default .htaccess configuration. This means that TYPO3 pages can now officially use the URL path /uploads.

It is recommended to apply these adjustments in existing TYPO3 installations as well, even for other web servers such as nginx or IIS, provided there is no custom usage of /_assets/ or /uploads/ (for example through a PSR-15 middleware, custom extension, or custom routing).

Apache example:

In Apache-based setups, look for this line:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|uploads/) - [L]
Copied!

and replace it with:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|_assets/) - [L]
Copied!

Important: #105310 - Create CHAR and BINARY as fixed-length columns 

See forge#105310

Description 

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with \TYPO3\CMS\Core\Schema\DefaultTcaSchema information for TCA managed tables and fields.

Fixed- and variable-length variants have been parsed in the past, but failed to flag the column as $fixed = true for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

ext_tables.sql created as (before) created as (now)
CHAR(10) VARCHAR(10) CHAR(10)
VARCHAR(10) VARCHAR(10) VARCHAR(10)
BINARY(10) VARBINARY(10) BINARY(10)
VARBINARY(10) VARBINARY(10) VARBINARY(10)

Not all relational database management systems (RDBMS) behave the same way for fixed-length columns. Implementation differences need to be respected to ensure consistent query and data behaviour across all supported database systems.

Fixed-length CHAR 

Key difference between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data. CHAR, which stands for character, is a fixed-length data type. It always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters (not just 5). Unused characters are padded with spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. Thus, storing the word apple in a VARCHAR(10) column will only occupy 5 characters.

The main difference between PostgreSQL and MySQL, MariaDB or SQLite is that PostgreSQL also returns the padded spaces for values that do not fill the full defined length (for example, apple[space][space][space][space] [space]).

In addition, these padded spaces are respected in query conditions, sorting or calculations (such as concat()). These differences make a significant impact and must be considered when using CHAR fields.

Rule of thumb for fixed-length CHAR columns

  • Use only with guaranteed fixed-length values to avoid padding.
  • For 255 or more characters, VARCHAR or TEXT must be used.

More hints for fixed-length CHAR columns

  • Ensure that stored values are fixed-length (non-space characters), for example by using hash algorithms that produce fixed-length identifiers.
  • Ensure that query statements use trim or rightPad within WHERE, HAVING or SELECT operations when values are not guaranteed to be fixed-length.

Example of differences in behaviour of fixed-length CHAR types 

The following examples illustrate how different relational database management systems handle fixed-length CHAR values, and why the behaviour must be carefully considered when storing or querying such data.

Creating a fixed-length field 
Example ext_tables.sql defining a fixed-length tt_content field
CREATE TABLE `tt_content` (
    `some_label` CHAR(10) DEFAULT '' NOT NULL,
);
Copied!
Inserting example data 

Two example rows are added below: one value fits exactly 10 characters, the other uses only 6 characters.

Adding two example rows
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionForTable('tt_content');
// adding a value with 10 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'some-label',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
// adding a value with only 6 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'label1',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
Copied!
Retrieving the records 
Get all records from table
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!
Result differences across platforms 

The returned values differ depending on the database platform.

Result rows MySQL, MariaDB or SQLite
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        'some_label' => 'label1',
    ],
];
Copied!
Result rows with PostgreSQL
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        // PostgreSQL applies the fixed length to the value directly,
        // filling it up with spaces
        'some_label' => 'label1    ',
    ],
];
Copied!
Result rows difference between database platforms (commented)
 <?php

 $rows = [
     [
         'uid' => 1,
         'some_label' => 'some-label',
     ],
     [
         'uid' => 2,
-        'some_label' => 'label1',      // MySQL, MariaDB, SQLite
+        'some_label' => 'label1    ',  // PostgreSQL
     ],
 ];
Copied!
Querying trimmed versus padded values 

Using a trimmed value in a WHERE clause can match the row, but the returned value will differ depending on the database platform.

Retrieve with trimmed value
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'), // trimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1    ']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
Copied!
Enforcing trimmed values in queries 
Retrieve with enforced trimmed value.
<?php

use Doctrine\DBAL\Platforms\TrimMode;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->as(
            $queryBuilder->expr()->trim(
                'fixed_title',
                TrimMode::TRAILING,
                ' '
            ),
            'fixed_title',
        ),
    )
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'),
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
// and ensures the same content across all supported database systems.
Copied!
Querying space-padded values in PostgreSQL 
Retrieve with space-padded value for PostgreSQL does not retrieve the record
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// PostgreSQL specific query!

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1    '), // untrimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows === []
Copied!

Additional tools for consistent behaviour 

Additional ExpressionBuilder methods can be used to ensure consistent behaviour across all supported platforms:

  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::trim()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::rightPad()

Recommendation 

CHAR and BINARY fields can be used for storage or performance adjustments, but only when composed data and queries account for the differences between database systems.

Otherwise, the safe bet is to consistently use VARCHAR and VARBINARY column types.

Important: #105538 - Plugin subtypes removed: Changes to configurePlugin() and TCA handling 

See forge#105538

Description 

Due to the removal of the plugin content element "Plugin" (list) and the corresponding plugin subtype field list_type, the fifth parameter $pluginType of ExtensionUtility::configurePlugin() is now unused and can be omitted. It is only kept for backwards compatibility.

Be aware that passing any value other than CType will trigger an \InvalidArgumentException.

Please also note that due to the removal of the list_type field in tt_content, passing list_type as the second parameter $field to ExtensionManagementUtility::addTcaSelectItemGroup() will now, as for any other non-existent field, trigger a \RuntimeException.

Important: #106192 - Add 'center' and 'font' to YAML processing removeTags 

See forge#106192

Description 

The HTML tags <font> and <center> have been officially deprecated for some time: see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/font and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/center.

The default YAML processing configuration file EXT:rte_ckeditor/Configuration/RTE/Processing.yaml has been changed to remove these HTML tags <font> and <center> by default when saving RTE field contents to the database.

This new default is adjusted with the option processing.HTMLparser_db.removeTags, which now also lists these two tags.

A stored input like <p><font face="Arial">My text</font></p> will - when saved - be changed to <p>My text</p>.

Affected installations 

All installations having <font> and <center> stored in their database fields, and where no custom RTE YAML configuration is in place that allows these tags.

Please note that due to issue forge#104839, this removeTags option was never properly applied previously, so the chances are that an installation never had output for font and center properly working anyway.

Also, note that CKEditor by default uses <span style="..."> tags to apply font formatting when using the Full preset.

Thus, the real-life impact should be low, but for legacy installations, you may want to convert existing data to replace font/html tags with their appropriate modern counterparts.

Migration 

Either accept the removal of these tags and use specific HTML tags like <span> and <div> to apply formatting.

Or adapt the RTE processing via TypoScript/YAML configuration to not have center and font in the processing.HTMLparser_db.removeTags list.

If the tags center and font have been configured via the editor.conf.style.definitions YAML option (not set by default), CKEditor would allow the use of these tags, but they will now be removed both when saving or when being rendered in the frontend. So these style definitions should be removed and/or adapted to <span style="..."> configurations.

Important: #106532 - Changed database storage format for Scheduler Tasks 

See forge#106532

Description 

TYPO3's system extension scheduler has stored its tasks to be executed in a PHP-serialized format in the database since its inception.

This has led to many problems, for example, when changing a class property to be fully typed, when a class name changed to use PHP 5 namespaces, or when renaming a class or a class property.

This has now changed: the task object now stores the "tasktype" (typically the class name) and its options in a "parameters" JSON-encoded value, as well as the execution details (database field execution_details) in separate fields of the database table tx_scheduler_task. This way, the task object can be reconstituted through a properly defined API, avoiding issues in the future.

All existing tasks are compatible with the new internal format. An upgrade wizard ensures that the previously serialized objects are transferred into the new format. If this wizard does not disappear after being executed, it means there are tasks that failed to migrate and may need manual inspection or recreation. Inspect all tasks in tx_scheduler_task where the "tasktype" column is empty. The old serialized data format is somewhat human-readable (or can be inspected with PHP deserializers), so recreating a task with its former configuration options should be possible.

Please note that this upgrade step needs to be performed in the context of TYPO3 v14. Running the wizard in future TYPO3 versions may not succeed due to changes in the task objects.

Important: #106649 - Default language binding in layout module 

See forge#106649

Description 

The Content > Layout module now always uses default language binding ( mod.web_layout.defLangBinding) when displaying localized content in language comparison mode.

Default language binding makes editing translations easier: Editors can see what they are translating next to the default language. It also prevents confusion when localizations are incomplete. Editors can directly see which content is not yet translated. This improves the user experience in the backend for multilingual sites, which was also a result of recent "Jobs To Be Done" (JTBD) interviews.

Impact 

Editors will now always see the content elements next to each other within the Content > Layout module when in language comparison mode.

Migration 

Because default language binding is now always enabled, the previous Page TSconfig setting mod.web_layout.defLangBinding is not evaluated and can therefore be removed.

Important: #106656 - Allow DEFAULT NULL for varchar fields 

See forge#106656

Description 

In TCA, if an input field is configured to be nullable via 'nullable' => true, the database migration now respects this and creates or updates existing fields with DEFAULT NULL.

In Extbase, this may cause issues if properties and their accessor methods are not properly declared as nullable. Therefore, this change is introduced only in TYPO3 v14.

Example:

Example properly implementing a nullable property
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyExtbaseEntity extends AbstractEntity
{
    protected ?string $title;

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(?string $title): void
    {
        $this->title = $title;
    }
}
Copied!

As stated above, this automatic detection is not provided in TYPO3 versions earlier than 14.0. Using DEFAULT NULL can be enforced via an extension's ext_tables.sql instead:

CREATE TABLE tx_myextension_table (
    title varchar(255) DEFAULT NULL
);
Copied!

Important: #106947 - Allow extensions to ship upgrade wizards without requiring EXT:install 

See forge#106947

Description 

EXT:install is no longer installed per default in composer based minimal installations.

However, this advantage could often not be utilised sensibly, since extensions still needed to require EXT:install as a dependency in order to ship upgrade wizards, because the implemented interfaces needed to be available.

The basic required interfaces and the PHP attribute are now moved to the EXT:core namespace. The old counterparts are deprecated with Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core but are still provided by EXT:install for the time being.

The following changes are required (when requiring TYPO3 14.0+) to make EXT:install optional:

  • Let custom upgrade wizards implement \TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface .
  • Implement custom list-type to CType migrations extending \TYPO3\CMS\Core\Upgrades\AbstractListTypeToCTypeUpdate .
  • Use the attribute \TYPO3\CMS\Core\Attribute\UpgradeWizard to register custom upgrade wizards.

Extension authors supporting two major TYPO3 versions with one extension version can follow these strategies:

  • TYPO3 v13 and v14: Use deprecated EXT:install interfaces and PHP Attribute and require EXT:install as mandatory dependency, or add it as a suggestion to allow the decision to be made on project-level.
  • TYPO3 v14: Switch to EXT:core interfaces and the new PHP Attribute.

See Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core for details about moved interfaces, PHP attribute and a migration example.

Important: #107328 - $GLOBALS['TCA'] in base TCA files 

See forge#107328

Description 

The backward compatibility for using $GLOBALS['TCA'] in base TCA files has been removed. Base TCA files are the first TCA files loaded by the Core and do not have the fully loaded TCA available yet. Until now, this worked because the Core temporarily populated this global array during the loading process.

Note that the usage of $GLOBALS['TCA'] in base TCA files was never explicitly allowed or disallowed, only discouraged. It worked only due to internal system knowledge by the user, for example, knowing that the loading order of extensions affects when those files are loaded. The only place this array should be used is inside TCA/Overrides/*.php files. For all other cases, the TcaSchema should be preferred.

Migration 

In the uncommon case that you find usages of $GLOBALS['TCA'] in base TCA files, move that access to TCA/Overrides/*.php files instead.

Important: #107399 - Add more common file types to mediafile_ext 

See forge#107399

Description 

The default configuration for $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] has been extended with additional formats commonly uploaded to web-based CMS systems such as TYPO3:

  • 3gp
  • aac
  • aif
  • avif
  • heic
  • ico
  • m4a
  • m4v
  • mov
  • psd

This list is extended only for systems where the configuration option has not already been customized.

Adding these formats allows files with these extensions to be uploaded into the file list (and used in TCA fields with "common-media-types") when the security flag security.system.enforceAllowedFileExtensions is enabled.

This may be a relevant change if these file types are now used in records or content elements that do not expect such files for further operations (for example, in frontend display).

Integrators must ensure that all uploaded file types are handled appropriately (for example, embedding "mov" as a video, "ico" as an image, or "psd" as a download) or have a suitable fallback.

The <f:media> ViewHelper, for example, iterates over possible Renderer implementations that can handle several of these file types (and their MIME types) and attempts to render a file as an image as a fallback.

Important: #107536 - Install Tool now adapts to backend login routing 

See forge#107536

Description 

The Install Tool now integrates with TYPO3's backend routing system instead of using a separate typo3/install.php file. This modernization improves consistency while maintaining full backward compatibility.

If the TYPO3 installation is not working properly, the Install Tool can now be accessed via the ?__typo3_install parameter, ensuring administrators can rely on it for system maintenance and recovery.

Impact 

For Administrators:

All existing workflows continue to work without changes. However, the Install Tool is now accessible via:

  • The __typo3_install parameter (for example, https://example.com/?__typo3_install)
  • Backend routes such as /typo3/install and /typo3/install.php still work. If the $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] is set, the Install Tool adapts accordingly.

System Recovery:

When the TYPO3 installation is not set up or not working properly, the magic __typo3_install parameter still redirects to the installer or maintenance tool as before, ensuring administrators can always access system recovery tools.

Technical Benefits:

The Install Tool now uses the same routing infrastructure as the rest of the TYPO3 backend, creating a more unified and maintainable architecture while supporting the long-term goal of simplifying TYPO3's directory structure.

Migration 

No migration is required. All existing documentation, scripts, and workflows using the Install Tool continue to function without modification.

Important: #107629 - Reference Index check moved to Install Tool 

See forge#107629

Description 

The Check and Update Reference Index functionality has been moved from the System > DB Check module (EXT:lowlevel) to the Maintenance section of the Install Tool (EXT:install) in the top level module now called System.

This change makes this essential administrative tool more accessible and better organized alongside other common maintenance tasks such as database comparison, cache management, and folder structure checks.

Why this change? 

The Reference Index is a critical system component that tracks relationships between records in TYPO3. Checking and updating is a routine maintenance task that administrators perform regularly, similar to:

  • Analyzing database structure
  • Clearing caches
  • Checking folder permissions

Previously, this functionality was hidden in the DB Check module of EXT:lowlevel, which made it:

  • Hard to discover: Administrators had to know where to look
  • Inconsistent: Other maintenance tools were in the Install Tool
  • Less accessible: Required an additional system extension

Impact 

For Administrators:

The Reference Index check and update functionality is now directly available in the Install Tool under Maintenance > Check and Update Reference Index.

Key benefits:

  • Better visibility: Found alongside other maintenance tools
  • No extra dependencies: Works out-of-the-box without EXT:lowlevel
  • Consistent location: All system maintenance tasks in one place
  • Faster access: Direct access via the Install Tool
  • Same functionality: Check and update operations work exactly as before

CLI Access:

The command-line interface remains unchanged and continues to work as before:

# Check reference index
vendor/bin/typo3 referenceindex:update --check

# Update reference index
vendor/bin/typo3 referenceindex:update
Copied!

Migration 

No migration is required. System Maintainers should use the new location in the System > Maintenance section instead of the System > DB Check module.

The functionality works identically to the previous implementation.

Important: #107735 - Internal methods removed from ResourceFactory 

See forge#107735

Description 

The following internal methods have been removed from \TYPO3\CMS\Core\Resource\ResourceFactory :

  • getDefaultStorage()
  • getStorageObject()
  • createFolderObject()
  • getFileObjectByStorageAndIdentifier()

These methods were marked as @internal and are replaced by using \TYPO3\CMS\Core\Resource\StorageRepository directly for better separation of concerns. However, some of these methods might have been used in custom extensions despite being marked as internal.

Migration 

Instead of using the removed methods from ResourceFactory, use the appropriate methods from StorageRepository or direct access to ResourceStorage:

// Before
$defaultStorage = $resourceFactory->getDefaultStorage();
$storage = $resourceFactory->getStorageObject($uid);
$folder = $resourceFactory->createFolderObject($storage, $identifier, $name);
$file = $resourceFactory->getFileObjectByStorageAndIdentifier($storage, $fileIdentifier);

// After
$defaultStorage = $storageRepository->getDefaultStorage();
$storage = $storageRepository->getStorageObject($uid);
$folder = $storage->getFolder($identifier);
$file = $storage->getFileByIdentifier($fileIdentifier);
Copied!

Important: #107789 - TCA tab labels consolidated into core.form.tabs 

See forge#107789

Description 

To improve consistency and maintainability of TCA tab labels across TYPO3 Core, commonly used tab labels from various extensions have been consolidated into the central EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf file.

This consolidation allows for better reusability and ensures consistent translation of common tab labels across all core extensions. It also makes it easier for extension developers to use standardized tab names.

New labels available in locallang_tabs.xlf 

The following new tab labels are now available and should be used via the core.form.tabs: prefix:

  • core.form.tabs:audio - "Audio"
  • core.form.tabs:video - "Video"
  • core.form.tabs:camera - "Camera"
  • core.form.tabs:permissions - "Permissions"
  • core.form.tabs:mounts - "Mounts"
  • core.form.tabs:personaldata - "Personal Data"

Previously existing labels (already migrated in core):

  • core.form.tabs:general - "General"
  • core.form.tabs:access - "Access"
  • core.form.tabs:categories - "Categories"
  • core.form.tabs:notes - "Notes"
  • core.form.tabs:language - "Language"
  • core.form.tabs:extended - "Extended"
  • core.form.tabs:appearance - "Appearance"
  • core.form.tabs:behaviour - "Behavior"
  • core.form.tabs:metadata - "Metadata"
  • core.form.tabs:resources - "Resources"
  • core.form.tabs:seo - "SEO"
  • core.form.tabs:socialmedia - "Social Media"
  • core.form.tabs:options - "Options"

Migrated extension-specific labels 

The following extension-specific tab labels have been migrated to the consolidated labels file and are marked as unused since TYPO3 v14.0 with the attribute x-unused-since="14.0" in the corresponding XLF files.

EXT:filemetadata

  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata core.form.tabs:metadata
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.camera core.form.tabs:camera
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.audio core.form.tabs:audio
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.video core.form.tabs:video

EXT:seo

  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.seo core.form.tabs:seo
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.socialmedia core.form.tabs:socialmedia

EXT:core - Backend Users

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data core.form.tabs:personaldata
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.options core.form.tabs:options

EXT:core - Backend User Groups

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.options core.form.tabs:options

EXT:frontend - Frontend Users

  • LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData core.form.tabs:personaldata

Affected installations 

Custom extensions using TCA configurations may benefit from using the new consolidated tab labels instead of creating their own labels for common tab names.

Extensions that were using any of the migrated extension-specific labels listed above will continue to work in TYPO3 v14.0, but should migrate to the consolidated labels. The old labels will be removed in TYPO3 v15.0.

Migration 

For custom extensions, consider using the consolidated core.form.tabs: labels instead of creating custom labels for common tab names.

Example migration for extensions using old labels:

File metadata tabs

// Before
'--div--;LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata'

// After
'--div--;core.form.tabs:metadata'
Copied!

User and group tabs

// Before
'--div--;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data'
'--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData'

// After
'--div--;core.form.tabs:personaldata'
Copied!

Using consolidated labels in custom extensions

// Example: Custom TCA using consolidated labels
'types' => [
    '1' => [
        'showitem' => '
            --div--;core.form.tabs:general,
                title, description,
            --div--;core.form.tabs:metadata,
                author, keywords,
            --div--;core.form.tabs:access,
                hidden, starttime, endtime,
            --div--;core.form.tabs:categories,
                categories,
            --div--;core.form.tabs:extended,
        ',
    ],
],
Copied!

New labels available in palettes.xlf 

In addition to tab labels, commonly used palette labels have also been consolidated into the central EXT:core/Resources/Private/Language/Form/palettes.xlf file.

The following palette labels are now available via the core.form.palettes: prefix:

  • core.form.palettes:general - "General"
  • core.form.palettes:account - "Account"
  • core.form.palettes:authentication - "Authentication"
  • core.form.palettes:permission_languages - "Language permissions"
  • core.form.palettes:permission_general - "General permissions"
  • core.form.palettes:permission_specific - "Specific permissions"
  • core.form.palettes:standard - "Page"
  • core.form.palettes:title - "Title"
  • core.form.palettes:visibility - "Visibility"
  • core.form.palettes:access - "Publish Dates and Access Rights"
  • core.form.palettes:abstract - "Abstract"
  • core.form.palettes:metatags - "Meta Tags"
  • core.form.palettes:editorial - "Editorial"
  • core.form.palettes:page_layout - "Page Layout"
  • core.form.palettes:use_as_container - "Use as Container"
  • core.form.palettes:replace - "Replace Content"
  • core.form.palettes:links - "Links to this Page"
  • core.form.palettes:caching - "Caching"
  • core.form.palettes:language - "Language"
  • core.form.palettes:miscellaneous - "Miscellaneous"
  • core.form.palettes:media - "Files"
  • core.form.palettes:storage - "Storage Page"
  • core.form.palettes:config - "Configuration"
  • core.form.palettes:headers - "Headlines"
  • core.form.palettes:header - "Header"
  • core.form.palettes:content_layout - "Content Element Layout"
  • core.form.palettes:media_behaviour - "Media Behaviour"
  • core.form.palettes:accessibility - "Accessibility"
  • core.form.palettes:downloads_layout - "Downloads Layout"
  • core.form.palettes:table_layout - "Table Layout"
  • core.form.palettes:links_appearance - "Links"
  • core.form.palettes:settings_gallery - "Gallery Settings"
  • core.form.palettes:media_adjustments - "Media Adjustments"
  • core.form.palettes:metrics - "Metrics"
  • core.form.palettes:geolocation - "Geo Location"
  • core.form.palettes:contentdate - "Content Date"
  • core.form.palettes:gps - "GPS"
  • core.form.palettes:seo - "General SEO settings"
  • core.form.palettes:robots - "Robot instructions"
  • core.form.palettes:opengraph - "Open Graph (Facebook)"
  • core.form.palettes:twittercards - "X / Twitter Cards"
  • core.form.palettes:canonical - "Canonical"
  • core.form.palettes:sitemap - "Sitemap"
  • core.form.palettes:additional - "Additional configuration"

Migrated palette labels 

The following palette labels have been migrated to use core.form.palettes:* and are marked as unused since TYPO3 v14.0 (attribute x-unused-since="14.0" in XLF files):

EXT:core - Backend Users & Groups:

  • be_users.palettes.account, be_users.palettes.authentication, be_users.palettes.permissionLanguages
  • be_groups.palettes.authentication, be_groups.palettes.permissionGeneral, be_groups.palettes.permissionLanguages, be_groups.palettes.permissionSpecific

EXT:frontend - Pages & Content Elements:

  • pages.palettes.* (17 labels: standard, title, visibility, access, abstract, metatags, editorial, layout, module, replace, links, caching, language, miscellaneous, media, storage, config)
  • palette.* in tt_content (13 labels: general, headers, header, visibility, access, frames, imagelinks, image_accessibility, uploads_layout, table_layout, appearanceLinks, gallerySettings, mediaAdjustments)

EXT:filemetadata:

  • palette.* (6 labels: metrics, geo_location, visibility, content_date, accessibility, gps)

EXT:seo:

  • pages.palettes.* (6 labels: seo, robots, opengraph, twittercards, canonical, sitemap)

Important: #107848 - DataHandler properties userid and admin removed 

See forge#107848

Description 

The internal properties \TYPO3\CMS\Core\DataHandling\DataHandler::$userid and \TYPO3\CMS\Core\DataHandling\DataHandler::$admin have been removed.

These properties contained information that is already available through the BE_USER property and were therefore redundant.

Impact 

Accessing these properties directly will result in a fatal PHP error.

Affected installations 

All installations with extensions that access the following properties:

  • \TYPO3\CMS\Core\DataHandling\DataHandler::$userid
  • \TYPO3\CMS\Core\DataHandling\DataHandler::$admin

While these properties were marked as @internal, they have been commonly used by extensions, especially the $admin property.

Migration 

Replace any usage of these properties with the appropriate methods from the BE_USER property.

For $userid 

// Before:
$userId = $dataHandler->userid;

// After:
$userId = $dataHandler->BE_USER->getUserId();
Copied!

For $admin 

// Before:
if ($dataHandler->admin) {
    // do something
}

// After:
if ($dataHandler->BE_USER->isAdmin()) {
    // do something
}
Copied!

14.x Changes by type 

This lists all changes to the TYPO3 Core of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

ChangeLog v13 

Every change to the TYPO3 Core which might affect your site is documented here.

Also available 

13.4.x Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.4.0 LTS release.

Features 

Deprecation 

Important 

Feature: #105638 - Modify fetched page content 

See forge#105638

Description 

With forge#103894 the new data processor PageContentFetchingProcessor has been introduced, to allow fetching page content based on the current page layout, taking the configured SlideMode into account.

Fetching content has previously mostly been done via the Content content object. A common example looked like this:

page.20 = CONTENT
page.20 {
    table = tt_content
    select {
        orderBy = sorting
        where = colPos=0
    }
}
Copied!

As mentioned in the linked changelog, using the page-content data processor, this can be simplified to:

page.20 = page-content
Copied!

This however reduces the possibility to modify the select configuration (SQL statement), used to define which content should be fetched, as this is automatically handled by the data processor. However, there might be some use cases in which the result needs to be adjusted, e.g. to hide specific page content, like it's done by EXT:content_blocks for child elements. For such use cases, the new PSR-14 AfterContentHasBeenFetchedEvent has been introduced, which allows to manipulate the list of fetched page content.

The following member properties of the event object are provided:

  • $groupedContent: The fetched page content, grouped by their column - as defined in the page layout
  • $request: The current request, which can be used to e.g. access the page layout in question

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, removes some of the fetched page content elements based on specific field values.

my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(AfterContentHasBeenFetchedEvent $event): void
    {
        foreach ($event->groupedContent as $columnIdentifier => $column) {
            foreach ($column['records'] ?? [] as $key => $record) {
                if ($record->has('parent_field_name') && (int)($record->get('parent_field_name') ?? 0) > 0) {
                    unset($event->groupedContent[$columnIdentifier]['records'][$key]);
                }
            }
        }
    }
}
Copied!

Impact 

Using the new PSR-14 AfterContentHasBeenFetchedEvent, it's possible to manipulate the page content, which has been fetched by the PageContentFetchingProcessor, based on the page layout and corresponding columns configuration.

Important: #92187 - Evaluation of incoming HTTP Header X-Forwarded-Proto 

See forge#92187

Description 

When running TYPO3 behind a reverse proxy, the site owner needs to set two TYPO3 settings.

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue'] = 'first';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '{ip-of-the-reverse-proxy}';
Copied!

At this point it is not known if the request between the client (the actual web browser for example) and the reverse proxy was made via HTTP or HTTPS, mainly because TYPO3 only evaluated the information from the reverse proxy to TYPO3 - which was typically faked on the TYPO3's webserver by setting "HTTPS=on" (for example via .htaccess file). In a typical setup, the communication between the reverse proxy and TYPO3's webserver is done via HTTP and irrelevant for TYPO3.

When the site owner knows that the reverse proxy acts as a SSL termination point and only communicates via https to the client, the $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] option can be set, to identify all reverse proxy IPs that ensure a secure connection between client and reverse proxy.

In case, it is not known, and reverseProxyPrefixSSL is not in use, but reverseProxyIP is in use, the incoming HTTP header X-Forwarded-Proto is now evaluated to determine if the request was made, if the header is sent.

If it is NOT sent, TYPO3 will assume to detect a secure connection between SSL information as before via various other HTTP Headers or server configuration settintgs.

Important: #103140 - Allow to configure rate limiters in Message consumer (Symfony Messenger) 

See forge#103140

Description 

This change introduces missing configuration options for Symfony Messenger-based rate limiters.

A rate limiter controls how frequently a specific event (e.g., HTTP request or login attempt) is allowed to occur. It acts as a safeguard to prevent services from being overwhelmed — either accidentally or intentionally — thus helping to maintain their availability.

Rate limiters are also useful for controlling internal or outbound processes, such as limiting the simultaneous processing of messages.

More information about the rate limiter is available in the Symfony Rate Limiter component documentation.

Usage 

Configure a rate limiter per queue 

Rate limiters can be defined in your service configuration EXT:yourext/Configuration/Services.yaml. The name specified in the settings is resolved to a service tagged with messenger.rate_limiter and the corresponding identifier.

Example Configuration:

EXT:yourext/Configuration/Services.yaml
messenger.rate_limiter.demo:
  class: 'Symfony\Component\RateLimiter\RateLimiterFactory'
  arguments:
    $config:
      id: 'demo'
      policy: 'sliding_window'
      limit: '100'
      interval: '60 seconds'
    $storage: '@Symfony\Component\RateLimiter\Storage\InMemoryStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'demo'

messenger.rate_limiter.default:
  class: 'Symfony\Component\RateLimiter\RateLimiterFactory'
  arguments:
    $config:
      id: 'default'
      policy: 'sliding_window'
      limit: '100'
      interval: '60 seconds'
    $storage: '@Symfony\Component\RateLimiter\Storage\InMemoryStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'default'
Copied!

Important: #104477 - Remove hyphen prefix from sys_log's data field entry 

See forge#104477

Description 

The \TYPO3\CMS\Core\Log\Writer\DatabaseWriter is used to write logs into the database table sys_log. Additional log information data is persisted in the field data and has been prefixed with a - until now. As this makes it harder to parse the data, which is JSON-encoded anyway, the prefix has been removed.

Beware that existing log entries are not migrated automatically. This leads to a mixed structure in the database table until old records are cleaned. (TYPO3 itself does not interpret the content of the field.)

Important: #105310 - Create CHAR and BINARY as fixed-length columns 

See forge#105310

Description 

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with DefaultTcaSchema information for TCA-managed tables and fields.

Fixed and variable length variants have been parsed already in the past, but missed to flag the column as $fixed = true for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

ext_tables.sql created as (before) created as (now)
CHAR(10) VARCHAR(10) CHAR(10)
VARCHAR(10) VARCHAR(10) VARCHAR(10)
BINARY(10) VARBINARY(10) BINARY(10)
VARBINARY(10) VARBINARY(10) VARBINARY(10)

Not all database systems (RDBMS) act the same way for fixed-length columns. Implementation differences need to be respected to ensure the same query/data behaviour across all supported database systems.

Fixed-length CHAR 

Key Difference Between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data in a database. CHAR, which stands for character, is a fixed-length data type, meaning it always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters worth of space (not just 5). Unusued characters are padded with extra spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. So, storing the word apple in a VARCHAR(10) column will only occupy 5 characters worth of space, leaving the remaining table row space available for other data.

The main difference from PostgreSQL to MySQL/MariaDB/SQLite is: PostgreSQL also returns the filler-spaces for a value not having the column length (returning apple[space][space][space][space][space]).

On top of that, the filled-up spaces are also respected for query conditions, sorting or data calculations ( concat() for example). These two facts makes a huge difference and must be carefully taken into account when using CHAR field.

Rule of thumb for fixed-length CHAR columns

  • Only use with ensured fixed-length values (so that no padding occurs).
  • For 255 or more characters VARCHAR or TEXT must be used.

More hints for fixed-length CHAR columns

  • Ensure to write fixed-length values for CHAR (non-space characters), for example use hash algorithms which produce fixed-length hash identifier values.
  • Ensure to use query statements to trim OR rightPad the value within WHERE, HAVING or SELECT operations, when values are not guaranteed to contain fixed-length values.

    Helper \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder expressions can be used, for example \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->trim() or \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->rightPad() to.

  • Usage of CHAR must be avoided when using the column with the Extbase ORM, because fixed-value length cannot be ensured due to the lack of using trim/rightPad within the ORM generated queries. Only with ensured fixed-length values, it is usable with Extbase ORM.
  • Cover custom queries extensively with functional tests executed against all supported database platforms. Code within public extensions should ensure to test queries and their operations against all officially TYPO3-supported database platforms.

Example of difference in behaviour of fixed-length CHAR types 

Example ext_tables.sql defining a fixed-length tt_content field
CREATE TABLE `tt_content` (
    `some_label` CHAR(10) DEFAULT '' NOT NULL,
);
Copied!

Now, add some data. One row which fits exactly to 10 characters, and one row that only uses 6 characters:

Adding two example rows
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionForTable('tt_content');
// adding a value with 10 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'some-label',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
// adding a value with only 6 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'label1',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
Copied!

Now see the difference in retrieving these records:

Get all records from table
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

Depending on the used database platform, the retrieved rows would contain these strings:

Result rows MySQL, MariaDB or SQLite
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        'some_label' => 'label1',
    ],
];
Copied!

but for PostgreSQL

Result rows with PostgreSQL
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        // PostgreSQL applies the fixed length to the value directly,
        // filling it up with spaces
        'some_label' => 'label1    ',
    ],
];
Copied!

or as a diff to make this even more visible:

Result rows difference between database platforms (commented)
 <?php

 $rows = [
     [
         'uid' => 1,
         'some_label' => 'some-label',
     ],
     [
         'uid' => 2,
-        'some_label' => 'label1',      // MySQL, MariaDB, SQLite
+        'some_label' => 'label1    ',  // PostgreSQL
     ],
 ];
Copied!

To raise the awareness for problems on this topic, using the trimmed value inside a WHERE condition will match the record, but the returned value will be different from the value used in the condition:

Retrieve with trimmed value
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'), // trimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1    ']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
Copied!
Retrieve with enforced trimmed value.
<?php

use Doctrine\DBAL\Platforms\TrimMode;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->as(
            $queryBuilder->expr()->trim(
                'fixed_title',
                TrimMode::TRAILING,
                ' '
            ),
            'fixed_title',
        ),
    )
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'),
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
// and ensures the same content across all supported database systems.
Copied!

On PostgreSQL, performing a query for a space-padded value will not actually return the expected row:

Retrieve with space-padded value for PostgreSQL does not retrieve the record
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// PostgreSQL specific query!

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1    '), // untrimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows === []
Copied!

Additional ExpressionBuilder methods can be used to ensure same behaviour on all platforms:

  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::trim()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::rightPad()

Recommendation 

CHAR and BINARY fields can be used (for storage or performance adjustments), but only when composed data and queries take care of database-system differences.

Otherwise, the "safe bet" is to consistently utilize VARCHAR and VARBINARY columns types.

Important: #105653 - Require a template filename in extbase module template rendering 

See forge#105653

Description 

With the introduction of the FluidAdapter in TYPO3 v13, the dependency between Fluid and Extbase has been decoupled. As part of this change, the behavior of the ModuleTemplate::renderResponse() and ModuleTemplate::render() methods has been adjusted.

The $templateFileName argument is now mandatory for the ModuleTemplate::renderResponse() and ModuleTemplate::render() methods. Previously, if this argument was not provided, the template was automatically resolved based on the controller and action names. Starting from TYPO3 13.4, calling these methods with an empty string or without a valid $templateFileName will result in an InvalidArgumentException.

Extensions using Extbase backend modules must explicitly provide the $templateFileName when calling these methods. Existing implementations relying on automatic template resolution need to be updated to prevent runtime errors.

Example:

Before:

$moduleTemplate->renderResponse();
Copied!

After:

$moduleTemplate->renderResponse('MyController/MyAction');
Copied!

Note, that it is already possible to explicitly provide the $templateFileName in TYPO3 12.4. It is therefore recommended to implement the new requirement for websites using TYPO3 12.4.

Important: #105703 - Premature end of script headers due to X-TYPO3-Cache-Tags 

See forge#105703

Description 

The X-TYPO3-Cache-Tags header is now split into multiple lines if it exceeds the maximum of 8000 characters. This change prevents premature end of script headers and ensures that the header is sent correctly, even if it contains a large number of cache tags.

Affected installations 

This change affects all TYPO3 installations that have $GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] enabled and misusing the X-TYPO3-Cache-Tags header for anything else then debugging. If you have a large number of cache tags, the header is now split into multiple lines to avoid exceeding the maximum header size limit imposed by some web servers. As this header is for debugging purposes only, this does not effect any production environments.

Important: #106401 - Treat 0 as a defined value for nullable datetime fields 

See forge#106401

Description 

For nullable integer-based datetime fields, the value 0 now explicitly represents the Unix epoch time (1970-01-01T00:00:00Z) instead of being interpreted as an empty value by FormEngine.

Only an explicit null database value will be considered an empty value.

The default database schema that is generated from TCA has been adapted to generate datetime columns with DEFAULT NULL instead of DEFAULT 0 if they have been configured to be nullable.

Given the following TCA definition:

'columns' => [
    'mydatefield' => [
        'config' => [
            'type' => 'datetime',
            'nullable' => true,
        ],
    ],
],
Copied!

The previously generated SQL statement will be changed from DEFAULT 0 to DEFAULT NULL:

Nullable datetime schema before this change
`mydatefield` bigint(20) DEFAULT 0
Copied!
Nullable datetime schema after this change
`mydatefield` bigint(20) DEFAULT NULL
Copied!

Fields that have not been explicitly configured to be nullable are unaffected and will default to 0 as before.

Important: #106467 - Align Extbase DateTime handling to FormEngine and DataHandler 

See forge#106467

Description 

Extbase handling of \DateTimeInterface domain model properties has been aligned with the persistence and database value interpretation behavior of the TYPO3 Core Engine (FormEngine and DataHandler).

Since this change addresses bugs and value interpretation differences that existed since the introduction of Extbase and there are many workarounds in use, a feature flag 'extbase.consistentDateTimeHandling' is introduced which allows to enable the new behavior.

Existing TYPO3 v13 instances will use the old behavior by default and are advised to enable the new feature flag via InstallTool or via:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['extbase.consistentDateTimeHandling'] = true;
Copied!

TYPO3 v14 (and new v13 instances) enable the consistent DateTime handling by default, but the feature can still be disabled manually, if needed for the time being.

There are four different behavioural changes that will be activated and are explained in the following sections.

Align persistence to database to match DataHandler algorithm 

Use the DataHandler algorithm for the mapping of DateTime objects to database values. This causes non-localtime timezone offsets in \DateTime objects (e.g. supplied by a frontend datepicker) to be respected for native datetime fields, like already done for integer based datetime fields. Note that the offset is not stored as-is, but mapped to PHP localtime, but the offset is no longer cropped off.

That means there is no need to force the server timezone on \DateTime objects before persisting an extbase model, since all dates will be normalized to localtime (for native datetime fields) or UTC (for interger based datetime fields) within the persistence layer.

Before:

public function setDatetime(\DateTime $datetime): void
{
    // Force local datetime zone in order to avoid
    // cropping non localtime offsets during persistence
    $datetime->setTimezone(
        new\DateTimeZone(date_default_timezone_get())
    );
    $this->datetime = $datetime;
}
Copied!

After:

public function setDatetime(\DateTime $datetime): void
{
    // No timezone enforcement needed, persistence layer will
    // persist correct point in time (UTC for integer, LOCALTIME for native
    // fields)
    $this->datetime = $datetime;
}
Copied!

Map date and datetime with named timezone instead of offset 

Extbase DataMapper converts dates of integer based database fields to \DateTime instances that use the current server date timezone (e.g., Europe/Berlin) and not just the time offset of the current server timezone (e.g., +01:00).

This prevents timezone shifts when modifying the resulting \DateTime object across daylight saving time boundaries.

Previous workarounds that explicitly added the server timezone for properties can be removed:

Before:

public function getDatetime(): ?\DateTime
{
    // object(DateTimeZone)#1 (2) {
    //   ["timezone_type"]=>
    //   int(1)
    //   ["timezone"]=>
    //   string(6) "+01:00"
    // }
    var_dump($this->datetime);

    $this->datetime->setTimezone(
        new\DateTimeZone(date_default_timezone_get())
    );

    return $this->datetime;
}
Copied!

After:

public function getDatetime(): ?\DateTime
{
    // object(DateTimeZone)#2 (2) {
    //   ["timezone_type"]=>
    //   int(3)
    //   ["timezone"]=>
    //   string(13) "Europe/Berlin"
    // }
    var_dump($this->datetime);

    // No explicit timezone needed for a proper named timezone
    return $this->datetime;
}
Copied!

Interpret integer based time fields as seconds without timezone offset 

The Extbase DataMapper will interpret format=time or format=timesec datetime fields as seconds without timezone offset, like FormEngine and DataHandler do. The database value is no longer considered as a UNIX timestamp, but as offset from midnight mapped on 1970-01-01T00:00:00 in PHP localtime.

For european timezones where Central Europe Time (CET) was active on 1970-01-01 that means an integer field value like 7200 (=02:00) will be mapped to 1970-01-01T02:00:00+01:00 instead of 1970-01-01T02:00:00+00:00 and the DateTime::$timezone property of the DateTime object will be set to the named timezone that is configured in PHP ini setting date.timezone instead of UTC.

That means the datetime value can be combined with explicit dates and is always using the server timezone.

Interpret 00:00:00 as non empty time value for nullable time properties 

Nullable format=time, format=timesec or dbType=time fields can now use 00:00:00 to represent midnight (this value has been used in non-nullable fields to represent an empty value). The DateTime mapper now understands this value instead of misinterpreting it as an empty value.

This behaviour could not be worked around before, that means existing implementations do not need to change or remove workarounds, but can basically support 00:00 as a value time field now.

Construct format=time and dbType=time properties based on 1970-01-01 

DateTime objects that map to native TIME fields or integer based fields configured with format=time are now initialized with 1970-01-01 as day-part instead of the current day which results in consistent mapped values independent from the day where the mapping is performed.

Before:

public function getDatetime(): ?\DateTime
{
    //object(DateTime)#2 (3) {
    //  ["date"]=>
    //  string(26) "2025-04-11 11:44:00.000000"
    //  ["timezone_type"]=>
    //  int(1)
    //  ["timezone"]=>
    //  string(6) "+02:00"
    //}

    var_dump($this->datetime);

    return $this->datetime;
}
Copied!

After:

public function getDatetime(): ?\DateTime
{
    //object(DateTime)#2 (3) {
    //  ["date"]=>
    //  string(26) "1970-01-01 11:44:00.000000"
    //  ["timezone_type"]=>
    //  int(3)
    //  ["timezone"]=>
    //  string(13) "Europe/Berlin"
    //}
    var_dump($this->datetime);

    return $this->datetime;
}
Copied!

Important: #106494 - Adapt custom instances of AbstractFormFieldViewHelper to deal with persistenceManager->getIdentifierByObject() methods 

See forge#106494

Description 

When dealing with relations to multilingual Extbase entities, these relations should always store their reference to the "original" (sys_language_uid=0) entity, so that later on, language record overlays can be properly applied.

For this to work in areas like persistence, internally an "identifier" is established that references these multilingual objects like [defaultLanguageRecordUid]_[localizedRecordUid].

Internally, this identifier should be converted back to only contain/reference the defaultLanguageRecordUid.

A bug has been fixed with #106494 to deal with this inside the <f:form.select> ViewHelper, which utilized an <option value="11_42"> (defaultLanguageRecordUid=11, localizedRecordUid=42), and when using an <f:form> to edit existing records, the currently attached records would NOT get pre-selected.

When such objects with relations were persisted (in frontend management interfaces with Extbase), if the proper option had not been selected again, the relation would get lost.

Important: Adapt custom ViewHelpers extended from AbstractFormFieldViewHelper or using persistenceManager->getIdentifierByObject() 

The bug has been fixed, but it is important that if third-party code created custom ViewHelpers based on \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper , these may need adoption too.

Instead of using code like this:

Example ViewHelper code utilizing persistenceManager->getIdentifierByObject()
if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
    return $this->persistenceManager->getIdentifierByObject($valueElement);
}
Copied!

the code should be adopted to not rely on getIdentifierByObject() but instead:

Refactored ViewHelper code preferring an object's getUid() method instead
if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
    if ($valueElement instanceof DomainObjectInterface) {
        return $valueElement->getUid() ?? $this->persistenceManager->getIdentifierByObject($valueElement);
    }
    return $this->persistenceManager->getIdentifierByObject($valueElement);
}
Copied!

This code ensures that retrieving the relational object's UID is done with the overlaid record, and only falls back to the full identifier, if it's not set, or not an object implementing the Extbase DomainObjectInterface.

Also note that the abstract's method convertToPlainValue() has been fixed to no longer return a value of format [defaultLanguageRecordUid]_[localizedRecordUid] but instead always use the original record's ->getUid() return value (=defaultLanguageRecordUid).

If this method convertToPlainValue() is used in 3rd-party code, make sure this is the expected result, too.

Important: #106508 - Respect column CHARACTER SET and COLLATE in ext_tables.sql 

See forge#106508

Description 

TYPO3 now reads column based CHARACTER SET and COLLATION from extension ext_tables.sql files and applies them on column level. This allows CHARACTER SET and COLLATION column settings different than defaults defined on table or schema level. This is limited to MySQL and MariaDB DBMS.

For now, CHARACTER SET ascii COLLATE ascii_bin is used for sys_refindex.hash to reduce required space for the index using single bytes instead of multiple bytes per character.

The introduced database change is considerable non-breaking, because:

  • Not applying the database changes still keeps a fully working state.
  • Applying database schema change does not require data migrations.
  • Targets only MySQL and MariaDB.
ext_tables.sql example
CREATE TABLE some_table (

    col1    CHAR(10) DEFAULT ''             NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col2    CHAR(10) CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col3    VARCHAR(10) DEFAULT ''          NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col4    VARCHAR(10) CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col5    TEXT DEFAULT ''                 NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col6    TEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col7    MEDIUMTEXT DEFAULT ''           NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col8    MEDIUMTEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col9    LONGTEXT DEFAULT ''             NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col10   LONGTEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
Copied!

);

Important: #106894 - Site settings.yaml is now stored as a map 

See forge#106894

Description 

Site settings are defined as a map of keys, with a defined type and default.

The values were previously stored as a tree representation in settings.yaml, e.g.:

EXT:my_extension/Configuration/Sets/MySet/settings.yaml
foo:
  bar: 'value'
Copied!

This tree representation is easier to write, but has the drawback that arbitrary keys like foo.bar and foo.bar.baz exclude each other, as the subkey baz would be represented as a value of foo.bar in tree representation.

Note that TypoScript constants can express subkey constants since TypoScript can store a value and childnodes for every node, which means that existing extensions – that migrate to site sets – require this mixture of setting identifiers to be supported in order to avoid breaking existing settings.

The storage format of settings.yaml is now changed to use a map (like settings.definitions.yaml already do) to store setting values, in order to overcome the mentioned limitation. It is still supported to read from a tree, but the settings editor will convert the tree to a map when persisting values.

Given the following setting definition:

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  foo.bar:
    type: string
    default: ''
    label: FooBar
  foo.bar.baz:
    type: string
    default: ''
    label: FooBarBaz
Copied!

A map will be stored in settings.yaml that is able to store values for both setting identifiers:

typo3conf/sites/mysite/settings.yaml
foo.bar: 'Foo bar value'
foo.bar.baz: 'Foo Bar baz value'
Copied!

Also site sets are advised to use this format for settings provided in their sets settings.yaml file.

Existing anonymous settings (pre v13 style, e.g. settings without a matching settings.definitions.yaml definition) will be preserved as a tree, since it is not known which tree node is key or a value.

Important: #107062 - Avoid applying Content-Security-Policy nonce sources when not required 

See forge#107062

Description 

Using nonce sources in a Content-Security-Policy (CSP) HTTP header implicitly leads to having a Cache-Control: private, no-store HTTP response header and internally requires to renew the nonce value that is present in cached HTML contents, which has a negative impact on performance.

This change aims for having fully cached pages and tries to avoid nonce sources in the CSP header when actually feasible.

  • The ConsumableNonce class was refactored – it no longer extends ConsumableString.
  • Two new counters are introduced: consumeInline() – the nonce is required for an inline resource. consumeStatic() – the nonce is optional for a static resource.

Example usage:

<?php
$nonce = new ConsumableNonce();
$nonce->consumeInline(Directive::ScriptSrcElem); // inline script
$nonce->consumeStatic(Directive::StyleSrcElem);  // static style
Copied!

Nonce sources are removed from the CSP policy in the following situations, in case the request is supposed to be fully cacheable (config.no_cache = 0 and not having any USER_INT or COA_INT items):

  • The response body is readable and contains no bytes.
  • The nonce consumption counter for all usages equals zero.
  • A directive contains a source‑keyword exception (e.g. 'unsafe-inline') that makes a nonce unnecessary.
  • The PolicyPreparedEvent has been dispatched and explicitly tells the policy to avoid using nonce sources.

When the nonce should be removed, both the frontend and backend ContentSecurityPolicyHeaders middleware strip the nonce-related literals from the rendered HTML.

New PSR-14 event 

<?php
declare(strict_types=1);

namespace Example\MyPackage\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyPreparedEvent;

#[AsEventListener('my-package/content-security-policy/avoid-nonce')]
final class DropNonceEventListener
{
    public function __invoke(PolicyPreparedEvent $event): void
    {
        $policyBag = $event->policyBag;
        if (
            isset($policyBag->dispositionMap[Disposition::enforce])
            && $policyBag->scope->siteIdentifier === 'my-special-site'
            // YOLO: drop nonce sources, even it is consumed
            && $policyBag->nonce->count() > 0
        ) {
            $policyBag->behavior->useNonce = false;
        }
    }
}
Copied!

New useNonce property 

The \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior class now contains the nullable boolean property useNonce:

  • true - explicitly allows using nonce sources
  • null - the default unspecific state (the system will detect and decide automatically)
  • false - explicitly denies using nonce sources, it also drops constraints like 'strict-dynamic' since that source keyword requires a nonce source

Important: #107063 - CKEditor 5 v46.1.0: TypeScript imports and CSS changes 

See forge#107063

Description 

With the upgrade to CKEditor 5 v46.1.0, three relevant changes are made:

  1. The API naming of TypeScript type imports has changed a lot. Any custom CKEditor 5 plugin using TypeScript type imports in their build chain will need to be adapted to match these imports. See https://ckeditor.com/docs/ckeditor5/latest/updating/nim-migration/migrating-imports.html for a large table of "before->after" renames. This is not considered a breaking change in context of TYPO3 integration, because existing JavaScript modules will continue to work, as TypeScript type imports are not part of the final output. Runtime imports that are exposed by the @ckeditor5/ckeditor-* modules have not been changed and will continue to work.
  2. A new opinionated default CSS is used by CKEditor to apply some improved styling over contents displayed within the RTE interface. Most of these are overruled by TYPO3's default CSS integration though. Possible customizations need to respect this.
  3. A few CSS classes have been renamed, see https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-46.html. These are for example referenced in custom CKEditor YAML configurations like the following diff, and need to replace the color subkey:

    Configuration/RTE/Full.yaml - Before/After
     - {
         model: 'yellowMarker',
         class: 'marker-yellow',
         title: 'Yellow marker',
         type: 'marker',
    -    color: 'var(--ck-highlight-marker-yellow)'
    +    color: 'var(--ck-content--highlight-marker-yellow)'
       }
     - {
         model: 'greenMarker',
         class: 'marker-green',
         title: 'Green marker',
         type: 'marker',
    -    color: 'var(--ck-highlight-marker-green)'
    +    color: 'var(--ck-content-highlight-marker-green)'
       }
     - {
         model: 'redPen',
         class: 'pen-red',
         title: 'Red pen',
         type: 'pen',
    -    color: 'var(--ck-highlight-pen-red)'
    +    color: 'var(--ck-content-highlight-pen-red)'
       }
    Copied!

Affected installations 

TYPO3 installation relying on custom or third-party CKEditor 5 TypeScript build chains, or CSS adaptations that no longer match the CKEditor 5 naming.

Possible Migration 

Follow the CKEditor 5 upgrade guide to change CSS class names and TypeScript imports.

Important: #107342 - Extend listForms method in FormPersistenceManagerInterface 

See forge#107342

Description 

With this change, the method signature of listForms(), defined by the FormPersistenceManagerInterface, has been extended by two arguments: $orderField and $orderDirection. The new definition is: public function listForms(array $formSettings, string $orderField = '', ?SortDirection $orderDirection = null): array;

Affected Installations 

Some TYPO3 installations may use this interface for their own FormPersistenceManager, even though it is marked as internal.

Possible Migration 

If you have implemented your own FormPersistenceManager, you need to update the method signature accordingly.

Important: #107594 - Icon overlay for TCA select items 

See forge#107594

Description 

The ability to define an icon overlay for items in the "New Content Element" wizard was originally introduced in forge#92942 using Page TSconfig, but was accidentally removed during the web-component migration in forge#100065 and then restored in forge#105253.

In the meantime, forge#102834 added auto-registration of wizard items directly from TCA. Since icon overlays defined in Page TSconfig duplicate configuration that can now be specified in TCA, the recommended approach is to define icon overlays directly in TCA using the new iconOverlay option for select items.

The iconOverlay property is now supported in the SelectItem component, enabling icon overlays for wizard items that are auto-registered via TCA.

Impact 

Icon overlays for New Content Element Wizard items can now be defined directly in TCA alongside other item properties like icon, label, description, and group.

This consolidates configuration in a single location and eliminates the need for separate Page TSconfig definitions. Page TSconfig icon overlays remain supported for backward compatibility, but TCA-based configuration is now the recommended approach.

Migration 

Previous approach using Page TSconfig (still works, but no longer recommended):

EXT:my_extension/Configuration/page.tsconfig
mod.wizards.newContentElement.wizardItems {
    my_group.elements {
        my_element {
            iconIdentifier = content-header
            iconOverlay = actions-approve
            title = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_title
            description = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_description
            tt_content_defValues {
                CType = my_element
            }
        }
    }
}
Copied!

Recommended approach using TCA:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_title',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_description',
        'value' => 'my_element',
        'icon' => 'content-header',
        'iconOverlay' => 'actions-approve',
        'group' => 'my_group'
    ],
    '...',
);
Copied!

Important: #107649 - Dependency Injection cache is now PHP version dependant 

See forge#107649

Description 

TYPO3 uses the PHP library symfony/dependency-injection to build a dependency injection container that contains class factories for services used by TYPO3 or by installed extensions.

With the update to symfony/dependency-injection v7.3 – which may be installed in TYPO3 v13 composer mode – the created factories are optimized to use certain PHP language level features, if available, which result in a cache that is incompatible when used with older PHP versions.

In a scenario where the dependency injection cache is created in a CLI PHP process (e.g. PHP v8.4), this may result in a cache to be created that is incompatible with a Web PHP process (e.g. PHP v8.2), if the minor versions of the CLI and Web environments differ.

For this reason the major and minor PHP version numbers are now hashed into the dependency injection cache identifier, resulting in a possible cache-miss on the first web-request after a deployment, if the system was prepared via bin/typo3 with a CLI PHP process version that is different to the Web PHP version.

Make sure to configure the PHP CLI process version php -v to use the same version number as configured for the Web process. The Web process version can be introspected in the backend toolbar entry System Information > PHP Version.

Important: #107681 - Disabled state for ShortcutButton and DropDownButton 

See forge#107681

Description 

The ShortcutButton and DropDownButton classes in the TYPO3 backend button bar system have been enhanced with new methods to support disabled state functionality. This brings them in line with other button types that already support the disabled state.

Two new methods have been added to both classes:

  • isDisabled(): bool - Checks if the button is disabled
  • setDisabled(bool $disabled) - Sets the disabled state of the button

When a button is disabled, it is rendered with appropriate HTML attributes and CSS classes to indicate its non-interactive state. For ShortcutButton, the disabled state is applied to both the simple button rendering and the dropdown rendering modes.

Impact 

Extension developers can now programmatically disable shortcut and dropdown buttons in the TYPO3 backend, preventing user interaction when needed. This is particularly useful for:

  • Preventing operations during form initialization
  • Disabling buttons during async operations
  • Conditional button availability based on application state
  • Improving user experience on slow network connections

The disabled state is properly propagated through the rendering process:

  • For shortcut buttons rendered as GenericButton, the disabled attribute is added to the button element
  • For shortcut buttons rendered as DropDownButton, the disabled state is passed to the dropdown button

Migration 

No migration is required. This change is fully backward compatible as it only adds new optional functionality. Existing code will continue to work without modifications.

Example usage for disabling a shortcut button:

EXT:my_extension/Classes/Controller/MyController.php
$shortcutButton = $buttonBar->makeShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId])
    ->setDisabled(true);
$buttonBar->addButton($shortcutButton);
Copied!

Example usage for disabling a dropdown button:

EXT:my_extension/Classes/Controller/MyController.php
$dropdownButton = GeneralUtility::makeInstance(DropDownButton::class)
    ->setLabel('Actions')
    ->setIcon($iconFactory->getIcon('actions-menu'))
    ->setDisabled(true);
$dropdownButton->addItem($item1);
$dropdownButton->addItem($item2);
$buttonBar->addButton($dropdownButton);
Copied!

Example usage for checking disabled state:

EXT:my_extension/Classes/Controller/MyController.php
if ($shortcutButton->isDisabled()) {
    // Handle disabled state
}
Copied!

13.4 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

None since TYPO3 v13.3 release.

Deprecation 

Important 

Deprecation: #105076 - Plugin content element and plugin sub types 

See forge#105076

Description 

Historically, plugins have been registered using the list content element and the plugin subtype list_type field. This functionality has been kept for backwards compatibility reasons. However, since the release of TYPO3 v12.4, the recommended way to create a plugin is by using a dedicated content type (CType) for each plugin.

This old "General Plugin" approach has always been ugly from a UX perspective point of view since it hides plugin selection behind "General plugin" content element, forcing a second selection step and making such plugins something special.

Therefore, the plugin content element (list) and the plugin sub types field ( list_type) have been marked as deprecated in TYPO3 v13.4 and will be removed in TYPO3 v14.0.

Additionally, the related PHP constant TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_PLUGIN has been deprecated as well.

Impact 

Plugins added using TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() where the second parameter is list_type (which is still the default) will trigger a deprecation level log entry in TYPO3 v13 and will fail in v14.

Therefore, the same applies on using TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() (to configure the plugin for frontend rendering), where no fifth parameter is provided or where the fifth parameter is list_type ( ExtensionUtility::PLUGIN_TYPE_PLUGIN), which is still the default.

The extension scanner will report any usage of configurePlugin(), where less than the required five arguments are provided. Actually, the only valid value for the fifth parameter $pluginType is CType, for which the ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT constant can be used.

Affected installations 

Extensions registering plugins as list_type plugin sub type.

Migration 

Existing plugins must be migrated to use the CType record type. Extension authors must implement the following changes:

  • Register plugins using the CType record type
  • Create update wizard which extends \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate and add list_type to CType mapping for each plugin to migrate. The migration wizard for indexed_search in class \IndexedSearchCTypeMigration can be used as reference example.
  • Migrate possible FlexForm registration and add dedicated showitem TCA configuration
  • Migrate possible PreviewRenderer registration in TCA
  • Adapt possible content element wizard items in Page TSConfig, where list_type is used
  • Adapt possible content element restrictions in backend layouts or container elements defined by third-party extensions like ichhabrecht/content-defender .

Common example 

// Before

$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['my_plugin'] = 'pi_flexform';
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['my_plugin'] = 'pages,layout,recursive';

// After
$GLOBALS['TCA']['tt_content']['types']['my_plugin']['showitem'] = '<Some Fields>,pi_flexform,<Other Fields>';
Copied!

Deprecation: #105171 - INCLUDE_TYPOSCRIPT TypoScript syntax 

See forge#105171

Description 

The old TypoScript syntax to import external TypoScript files based on <INCLUDE_TYPOSCRIPT: has been marked as deprecated with TYPO3 v13 and will be removed in v14.

Integrators should switch to @import.

There are multiple reasons to finally phase out this old construct:

  • The <INCLUDE_TYPOSCRIPT: syntax is clumsy and hard to grasp, especially when combining multiple options. It is hard to learn for integrators new to the project.
  • The implementation has a high level of complexity, is only partially tested and consists of edge cases that are hard to decide on and even harder to change since that may break existing usages in hard to debug ways.
  • The syntax can have negative security impact when not used wisely by loading TypoScript from editor related folders like fileadmin/. The syntax based on @import has been designed more thoughtful in this regard.
  • Loading TypoScript files from folders relative to the public web folder is unfortunate and can have negative side effects when switching "legacy" based instances to composer.
  • The syntax based on @import has been introduced in TYPO3 v9 already and has been designed to stay at this point in time. With TYPO3 v12, the last missing feature of <INCLUDE_TYPOSCRIPT: has been made possible for @import as well: @import can be loaded conditionally by putting them into the body of a casual TypoScript condition.
  • TYPO3 v12 discouraged using <INCLUDE_TYPOSCRIPT: within the documentation and already anticipated a future deprecation and removal.

Impact 

When using TypoScript <INCLUDE_TYPOSCRIPT: syntax a deprecation level log entry in TYPO3 v13 is emitted. The syntax will stop working with TYPO3 v14 and will be detected as an "invalid line" in the TypoScript related Backend modules.

Affected installations 

Instances TypoScript syntax based on <INCLUDE_TYPOSCRIPT:.

Migration 

Most usages of <INCLUDE_TYPOSCRIPT: can be turned into @import easily. A few examples:

# Before
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/myMenu.typoscript">
# After
@import 'EXT:my_extension/Configuration/TypoScript/myMenu.typoscript'

# Before
# Including .typoscript files in a single (non recursive!) directory
<INCLUDE_TYPOSCRIPT: source="DIR:EXT:my_extension/Configuration/TypoScript/" extensions="typoscript">
# After
@import 'EXT:my_extension/Configuration/TypoScript/*.typoscript'

# Before
# Including .typoscript and .ts files in a single (non recursive!) directory
<INCLUDE_TYPOSCRIPT: source="DIR:EXT:my_extension/Configuration/TypoScript/" extensions="typoscript,ts">
# After
@import 'EXT:my_extension/Configuration/TypoScript/*.typoscript'
# Rename all files ending on .ts to .typoscript

# Before
# Including a file conditionally
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/user.typoscript" condition="[frontend.user.isLoggedIn]">
# After
[frontend.user.isLoggedIn]
    @import 'EXT:my_extension/Configuration/TypoScript/user.typoscript'
[END]
Copied!

There are a few more use cases that cannot be transitioned so easily since @import is a bit more restrictive.

As one restriction @import cannot include files from arbitrary directories like fileadmin/, but only from extensions by using the EXT: prefix. Instances that use <INCLUDE_TYPOSCRIPT: with source="FILE:./someDirectory/..." should move this TypoScript into a project or site extension. Such instances are also encouraged to look into the TYPO3 v13 "Site sets" feature and eventually transition towards it along the way.

@import allows to import files with the file ending .typoscript and .tsconfig. If you used any of the outdated file endings like .ts or .txt rename those files before switching to the @import syntax.

The @import feature does not support recursive directory inclusion, as it does not allow wildcards in directory paths. Having directories like TypoScript/foo and TypoScript/bar, each having .typoscript files, could be included using <INCLUDE_TYPOSCRIPT: source=DIR:EXT:my_extension/Configuration/TypoScript extensions="typoscript">, which would find such files in foo and bar, and any other directory. This level of complexity was not wished to allow in the @import syntax since it can make file includes more intransparent with too much attached magic. Instances using this should either reorganize their files, or have multiple dedicated @import statements. The need for recursive includes may also be mitigated by restructuring TypoScript based functionality using "Site sets".

The transition from <INCLUDE_TYPOSCRIPT: can be often further relaxed with these features in mind:

Deprecation: #105213 - TCA sub types 

See forge#105213

Description 

One of the main features of TCA are the record types. This allows to use a single table for different purposes and in different contexts. The most known examples of using record types are the "Page Types" of pages and the "Content Types" of tt_content. For every specific type of such table, it's possible to define the fields to be used and even manipulate them e.g. change their label.

A special case since ever has been the plugin registration. This for a long time has been done using the so called "sub types" feature of TCA. This is another layer below record types and allows to further customize the behaviour of a record type using another select field, defined via subtype_value_field as well as defining fields to be added - subtypes_addlist - or excluded - subtypes_excludelist - for the record type, depending on the selected sub type.

For a couple of version now, it's encouraged to register plugins just as standard content elements via the tt_content type field CType. Therefore, the special registration via the combination of the list record type and the selection of a sub type via the list_type field has already been deprecated with Deprecation: #105076 - Plugin content element and plugin sub types.

Since the "sub types" feature was mainly used for this scenario only, it has now been deprecated as well. Registration of custom types should therefore always be done by using record types. This makes configuration much cleaner and more comprehensible.

Impact 

Using subtype_value_field in a TCA types configurations will lead to a deprecation log entry containing information about where adaptations need to take place.

Affected installations 

All installations using the sub types feature by defining a subtype_value_field in a TCA types configuration, which is really uncommon as the feature was mainly used for plugin registration in the tt_content table only.

Migration 

Replace any subtype_value_field configuration with dedicated record types. Please also consider migrating corresponding subtypes_addlist and subtypes_excludelist definitions accordingly.

Before 

'ctrl' => [
    'type' => 'type',
],
'columns' => [
    'type' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A record type',
                    'value' => 'a_record_type'
                ]
            ]
        ]
    ],
    'subtype' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A sub type',
                    'value' => 'a_sub_type'
                ]
            ]
        ]
    ],
],
'types' => [
    'a_record_type' => [
        'showitem' => 'aField,bField',
        'subtype_value_field' => 'subtype',
        'subtypes_addlist' => [
            'a_sub_type' => 'pi_flexform'
        ],
        'subtypes_excludelist' => [
            'a_sub_type' => 'bField'
        ]
    ]
]
Copied!

After 

'ctrl' => [
    'type' => 'type',
],
'columns' => [
    'type' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A record type',
                    'value' => 'a_record_type'
                ],
                [
                    'label' => 'A sub type',
                    'value' => 'a_sub_type'
                ]
            ]
        ]
    ],
],
'types' => [
    'a_record_type' => [
        'showitem' => 'aField,bField'
    ],
    'a_sub_type' => [
        'showitem' => 'aField,pi_flexform'
    ]
]
Copied!

Deprecation: #105230 - TypoScriptFrontendController and $GLOBALS['TSFE'] 

See forge#105230

Description 

Class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

Impact 

Calling TypoScriptFrontendController methods, or accessing state from $GLOBALS['TSFE'] is considered deprecated.

Affected installations 

Various instances may still retrieve information from $GLOBALS['TSFE'] . Remaining uses should be adapted. The extension scanner will find possible matches.

To keep backwards compatibility in TYPO3 v13, some calls can not raise deprecation level log messages.

Migration 

See Breaking: #102621 - Most TSFE members marked internal or read-only for details on substitutions. In general, most state used by extensions has been turned into request attributes.

Deprecation: #105252 - DataProviderContext getters and setters 

See forge#105252

Description 

The backend layout related data object class \TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext has been turned into a data object using public constructor property promotion (PCPP). All setX() and getX() methods have been marked as deprecated in TYPO3 v13.4 and will be removed with TYPO3 v14.0. The class will be declared readonly in TYPO3 v14.0 which will enforce instantiation using PCPP. The class has been declared final since it is an API contract that must never be changed or extended. The constructor arguments will be declared non-optional in TYPO3 v14.0.

  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setPageId()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setTableName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setFieldName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setData()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setPageTsConfig()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getPageId()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getTableName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getFieldName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getData()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getPageTsConfig()

Impact 

Calling the getters or setters raises deprecation level log errors and will stop working in TYPO3 v14.0.

Affected installations 

This data object is only relevant for instances with extensions that add custom backend layout data providers using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] . There are few known extensions that do this. The extension scanner is not configured to find possible usages since the method names are too generic and would lead to too many false positives.

Migration 

Create new objects using PCPP with named arguments instead of the setters. Instances should be created using new():

// Before
$dataProviderContext = GeneralUtility::makeInstance(DataProviderContext::class);
$dataProviderContext
    ->setPageId($pageId)
    ->setData($parameters['row'])
    ->setTableName($parameters['table'])
    ->setFieldName($parameters['field'])
    ->setPageTsConfig($pageTsConfig);

// After
$dataProviderContext = new DataProviderContext(
    pageId: $pageId,
    tableName: $parameters['table'],
    fieldName: $parameters['field'],
    data: $parameters['row'],
    pageTsConfig: $pageTsConfig,
);
Copied!

Use the properties instead of the getters, example:

// Before
$pageId = $dataProviderContext->getPageId()
// After
$pageId = $dataProviderContext->pageId
Copied!

Deprecation: #105279 - Replace TYPO3 EnumType with Doctrine DBAL EnumType 

See forge#105279

Description 

TYPO3 did provide a custom Doctrine DBAL column type implementation for the native SQL type ENUM that was only compatible with MySQL and MariaDB connections.

The Doctrine DBAL Team implemented ENUM support with Release 4.2.0 in class \Doctrine\DBAL\Types\EnumType, only supporting MySQL and MariaDB as well.

TYPO3 removed its custom implementation with TYPO3 v13.4.0.

Class \TYPO3\CMS\Core\Database\Schema\Types\EnumType has been marked as deprecated and is replaced with an class alias of \EnumType. The alias will be removed with TYPO3 v14.

See Release 4.2.0

Impact 

doctrine/dbal >= 4.2.0 is incompatible with TYPO3 versions before v13.4.0. Composer-based instances using TYPO3 v13.3 or older should add an according conflict to their composer.json.

Affected installations 

Instances using the the ENUM type directly or by any third party extension using TYPO3 13.0 to 13.3 in Composer mode will break, when the doctrine/dbal Composer packages is updated to version 4.2.0 or newer.

Migration 

Upgrade (directly) to TYPO3 v13.4 or ensure to avoid updating Doctrine DBAL to 4.2.x or newer versions in Composer-based instances.

Replace \TYPO3\CMS\Core\Database\Schema\Types\EnumType type declarations with \Doctrine\DBAL\Types\EnumType.

Deprecation: #105297 - tableoptions and collate connection configuration 

See forge#105297

Description 

The possibility to configure default table options like charset and collation for the database analyzer has been introduced using the array $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['tableoptions'] with sub-array keys charset and collate. These were only used for MySQL and MariaDB connections.

Since TYPO3 v11 the tableoptions keys were silently migrated to defaultTableOptions, which is the proper Doctrine DBAL connection option for for MariaDB and MySQL.

Furthermore, Doctrine DBAL 3.x switched from using they array key collate to collation, ignoring the old array key with Doctrine DBAL 4.x. This was silently migrated by TYPO3, too.

These options and migration are now deprecated in favor of using the final array keys and will be removed with TYPO3 v15 (or later) as breaking change.

Impact 

Instances using the database connection options in $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['tableoptions'] array, or using the collate key will trigger a E_USER_DEPRECATED notification.

Affected installations 

All instances using the mentioned options.

Migration 

Review settings.php and additional.php and adapt the deprecated configuration by renaming affected array keys.

# before
'DB' => [
    'Connections' => [
        'Default' => [
            'tableoptions' => [
                'collate' => 'utf8mb4_unicode_ci',
            ],
        ],
    ],
],

# after
'DB' => [
    'Connections' => [
        'Default' => [
            'defaultTableOptions' => [
                'collation' => 'utf8mb4_unicode_ci',
            ],
        ],
    ],
],
Copied!

Important: #105175 - Move FrontendBackendUserAuthentication into EXT:frontend 

See forge#105175

Description 

The internal \TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication class, used for frontend requests while being logged in the backend has been moved from EXT:backend to EXT:frontend, since its dependencies are limited to EXT:core and EXT:frontend.

While for v13 a class alias mapping and a legacy notation for IDE's is available, the class is marked as @internal and therefore does not fall under TYPO3's Core API deprecation policy.

13.3 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

Deprecation 

Important 

Feature: #93100 - Allow to directly declare static route variables 

See forge#93100

Description 

Instead of having to use custom route aspect mappers, implementing \TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface , to avoid having &cHash= signatures being applied to the generated URL, variables now can be simply declared static in the corresponding route enhancer configuration.

Impact 

By using the new static route configuration directive, custom aspect mapper implementations can be avoided. However, static route variables are only applied for a particular variable name if

  • there is no aspect mapper configured - aspect mappers are considered more specific and will take precedence
  • there is a companion requirements definition which narrows the set of possible values, and should be as restrictive as possible to avoid potential cache flooding - static routes variables are ignored, if there is no corresponding requirements definition

Example 

routeEnhancers:
  Verification:
    type: Simple
    routePath: '/verify/{code}'
    static:
      code: true
    requirements:
      # only allows SHA1-like hex values - which still allows lots
      # of possible combinations - thus, for this particular example
      # the handling frontend controller should be uncached as well
      #
      # hint: if `static` is set, `requirements` must be set as well
      code: '[a-f0-9]{40}'
Copied!

As a result, using the URI query parameters &code=11f6ad8ec52a2984abaafd7c3b516503785c2072 would generate the URL https://example.org/verify/11f6ad8ec52a2984abaafd7c3b516503785c2072.

Feature: #99418 - Enable recycler by default 

See forge#99418

Description 

The TYPO3 system extension typo3/cms-recycler is now enabled by default for new TYPO3 installations.

Impact 

New composer-based TYPO3 installations based on the TYPO3 CMS Base Distribution, and new legacy installations (tarball / zip download) have the system extension recycler enabled by default.

Feature: #99510 - Add file embedding option to asset ViewHelpers 

See forge#99510

Description 

The ViewHelpers <f:asset.css> and <f:asset.script> have been extended with a new argument inline. If this argument is set, the referenced asset file is rendered inline.

Setting the argument will therefore load the file content of the defined href / src as inline style or script. This is especially useful for content elements which are used as first element on a page and need some custom CSS to improve the Cumulative Layout Shift (CLS).

Impact 

To add inline styles and scripts from a referenced file, the new inline argument can be set. For example, to add above-the-fold styles, the priority option can be set, which will put the file contents of EXT:sitepackage/Resources/Public/Css/my-hero.css as inline styles to the <head> section.

<f:asset.css identifier="my-hero" href="EXT:sitepackage/Resources/Public/Css/my-hero.css" inline="1" priority="1"/>
Copied!

To add JavaScript:

<f:asset.script identifier="my-hero" src="EXT:sitepackage/Resources/Public/Js/my-hero.js" inline="1" priority="1"/>
Copied!

Feature: #101252 - Introduce ErrorHandler for 403 errors with redirect option 

See forge#101252

Description 

The new error handler \TYPO3\CMS\Core\Error\PageErrorHandler\RedirectLoginErrorHandler has been added, which makes it possible to redirect the user to a configurable page.

Requesting a login-protected URL would usually return a generic HTTP 403 error in case of a missing fulfilled access permissions and the configuration typolinkLinkAccessRestrictedPages = NONE (default) is set.

By enabling this new handler via the site settings, the 403 response can be handled and a custom redirect can be performed.

The RedirectLoginErrorHandler allows to define a loginRedirectTarget, which must be configured to the page, where the login process is handled. Additionally, the loginRedirectParameter must be set to the URL parameter that will be used to hand over the original URL to the target page.

The redirect ensures that the original URL is added to the configured GET parameter loginRedirectParameter, so that the user can be redirected back to the original page after a successful login.

The error handler allows return_url or redirect_url as values for loginRedirectParameter. Those values are used in extensions like EXT:felogin or EXT:oidc.

The new error handler works (with some minor exceptions) similar to the "Forbidden (HTTP Status 403)" handler in TYPO3 extension plan2net/sierrha . It will still emit generic 403 HTTP error messages in certain scenarios, like when a user is already logged in, but the permissions are not satisfied.

Impact 

It is now possible to configure a login redirection process when a user has no access to a page and a 403 error is thrown, so that after login the originating URL is requested again. Previously, this required custom Middlewares or implementations of PageErrorHandlerInterface .

Feature: #101391 - Add base64 attribute to ImageViewHelper 

See forge#101391

Description 

The ViewHelpers <f:image> and <f:uri.image> now support the attribute base64="true" that will provide a possibility to return the value of the image's src attribute encoded in base64.

<f:image base64="true" src="EXT:backend/Resources/Public/Images/typo3_logo_orange.svg" height="20" class="pr-2" />
<img src="{f:uri.image(base64: 'true', src:'EXT:backend/Resources/Public/Images/typo3_logo_orange.svg')}">
Copied!

Will result in the according HTML tag providing the image encoded in base64.

<img class="pr-2" src="data:image/svg+xml;base64,PHN2...cuODQ4LTYuNzU3Ii8+Cjwvc3ZnPgo=" alt="" width="20" height="20">
<img src="data:image/svg+xml;base64,PHN2...cuODQ4LTYuNzU3Ii8+Cjwvc3ZnPgo=">
Copied!

This can be particularly useful inside \TYPO3\CMS\Core\Mail\FluidEmail or to prevent unneeded HTTP calls.

Feature: #101472 - Allow static routes to assets 

See forge#101472

Description 

It is now possible to configure static routes with the type asset to link to resources which are typically located in the directory EXT:my_extension/Resources/Public/.

config/sites/my-site/config.yaml
routes:
  -
    route: example.svg
    type: asset
    asset: 'EXT:backend/Resources/Public/Icons/Extension.svg'
Copied!

Note that the asset URL can be configured on a per-site basis. This allows to deliver site-dependent custom favicon or manifest assets, for example.

Impact 

Static routes to files shipped with extensions can now be configured in the site configuration.

Feature: #102255 - Option to skip URL processing in AssetRenderer 

See forge#102255

Description 

The \TYPO3\CMS\Core\Page\AssetCollector options have been extended to include an external flag. When set for asset files using $assetCollector->addStyleSheet() or $assetCollector->addJavaScript(), all processing of the asset URI (like the addition of the cache busting parameter) is skipped and the input path will be used as-is in the resulting HTML tag.

Example 

The following code skips the cache busting parameter ?1726090820 for the supplied CSS file:

$assetCollector->addStyleSheet(
    'myCssFile',
    PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:my_extension/Resources/Public/MyFile.css')),
    [],
    ['external' => true]
);
Copied!

Resulting in the following HTML output:

<link rel="stylesheet" href="/_assets/<hash>/myFile.css" />
Copied!

Impact 

Developers can now use the AssetCollector API to embed JavaScript or CSS files without any processing of the supplied asset URI.

Feature: #102353 - AVIF support for images generated by GIFBUILDER 

See forge#102353

Description 

GIFBUILDER, the image manipulation library for TypoScript based on GDlib, a PHP extension bundled into PHP, now also supports generating resulting files of type "avif".

AVIF is an image format, that is supported by most modern browsers, and usually has a better compression (= smaller file size) than jpg files.

Impact 

If defined via format=avif within a GifBuilder setup, the generated files are now AVIF files instead of png (the default).

It is possible to define the quality of a AVIF image similar to jpg images globally via $TYPO3_CONF_VARS['GFX']['avif_quality'] or via TypoScript's "quality" property on a per-image basis. Via TypoScript it is also possible to use the new property "speed" - see https://www.php.net/manual/en/function.imageavif.php for more details.

Example 

page.10 = IMAGE
page.10 {
  file = GIFBUILDER
  file {
    backColor = yellow
    XY = 1024,199
    format = avif
    quality = 44
    speed = 1

    10 = IMAGE
    10.offset = 10,10
    10.file = 1:/my-image.jpg
  }
}
Copied!

A new test in the Environment module / Install Tool can be used to check if the bundled GDlib extension of your PHP version supports the AVIF image format.

Feature: #102422 - Introduce CacheDataCollector Api 

See forge#102422

Description 

A new API has been introduced to collect cache tags and their corresponding lifetime. This API is used in TYPO3 to accumulate cache tags from page cache and content object cache.

The API is implemented as a new PSR-7 request attribute 'frontend.cache.collector', which makes this API independent from TSFE.

Every cache tag has a lifetime. The minimum lifetime is calculated from all given cache tags. By default, the lifetime of a cache tag is set to PHP_INT_MAX, so it expires many years in the future. API users must therefore define the lifetime of a cache tag individually.

The current TSFE API is deprecated in favor of the new API, as the current cache tag API implementation does not allow to set lifetime and extension authors had to work around it.

Example 

Add a single cache tag with 24 hours lifetime
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable', 86400)
);
Copied!
Add multiple cache tags with different lifetimes
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable_123', 3600),
    new CacheTag('tx_myextension_mytable_456', 2592000)
);
Copied!
Remove a cache tag
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123')
);
Copied!
Remove multiple cache tags
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123'),
    new CacheTag('tx_myextension_mytable_456')
);
Copied!
Get minimum lifetime, calculated from all cache tags
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->resolveLifetime();
Copied!
Get all cache tags
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->getCacheTags();
Copied!

The following event should only be used in code that has no access to the request attribute 'frontend.cache.collector', it is marked @internal and may vanish: It designed to allow passive cache-data signaling, without exactly knowing the current context and not having the current request at hand. It is not meant to allow for cache tag interception or extension.

Add cache tag without access to the request object
$this->eventDispatcher->dispatch(
    new AddCacheTagEvent(
        new CacheTag('tx_myextension_mytable_123', 3600)
    )
);
Copied!

Feature: #103511 - Introduce Extbase file upload and deletion handling 

See forge#103511

Description 

TYPO3 now provides an API for file upload- and deletion-handling in Extbase extensions, which allows extension developers to implement file uploads more easily into Extbase Domain Models.

The scope of this API is to cover some of the most common use cases and to keep the internal file upload and deletion process in Extbase as simple as possible.

The API supports mapping and handling of file uploads and deletions for the following scenarios:

  • Property of type FileReference in a domain model
  • Property of type \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> in a domain model

File uploads can be validated by the following rules:

  • minimum and maximum file count
  • minimum and maximum file size
  • allowed MIME types
  • image dimensions (for image uploads)

Additionally, it is ensured, that the filename given by the client is valid, meaning that no invalid characters (null-bytes) are added and that the file does not contain an invalid file extension. The API has support for custom validators, which can be created on demand.

To avoid complexity and maintain data integrity, a file upload is only processed if the validation of all properties of a domain model is successful. In this first implementation, file uploads are not persisted/cached temporarily, so this means in any case of a validation failure ("normal" validators and file upload validation) a file upload must be performed again by users.

Possible future enhancements of this functionality could enhance the existing #[FileUpload] attribute/annotation with configuration like a temporary storage location, or specifying additional custom validators (which can be done via the PHP-API as described below)

Nesting of domain models 

File upload handling for nested domain models (e.g. modelA.modelB.fileReference) is not supported.

File upload configuration with the FileUpload attribute 

File upload for a property of a domain model can be configured using the newly introduced \TYPO3\CMS\Extbase\Annotation\FileUpload attribute.

Example:

#[FileUpload([
    'validation' => [
        'required' => true,
        'maxFiles' => 1,
        'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
        'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
        'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
    ],
    'uploadFolder' => '1:/user_upload/files/',
])]
protected ?FileReference $file = null;
Copied!

All configuration settings of the \TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration object can be defined using the FileUpload attribute. It is however not possible to add custom validators using the FileUpload attribute, which you can achieve with a manual configuration as shown below.

The currently available configuration array keys are:

  • validation ( array with keys required, maxFiles, minFiles, fileSize, fileExtension, allowedMimeTypes, mimeType, imageDimensions, see File upload validation)
  • uploadFolder ( string, destination folder)
  • duplicationBehavior ( object, behaviour when file exists)
  • addRandomSuffix ( bool, suffixing files)
  • createUploadFolderIfNotExist ( bool, whether to create missing directories)

It is also possible to use the FileUpload annotation to configure file upload properties, but it is recommended to use the FileUpload attribute due to better readability.

Manual file upload configuration 

A file upload configuration can also be created manually and should be done in the initialize*Action.

Example:

public function initializeCreateAction(): void
{
    $mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class);
    $mimeTypeValidator->setOptions(['allowedMimeTypes' => ['image/jpeg']]);
    $fileExtensionValidator = GeneralUtility::makeInstance(FileExtensionValidator::class);
    $fileExtensionValidator->setOptions(['allowedFileExtensions' => ['jpg', 'jpeg']]);

    $fileHandlingServiceConfiguration = $this->arguments->getArgument('myArgument')->getFileHandlingServiceConfiguration();
    $fileHandlingServiceConfiguration->addFileUploadConfiguration(
        (new FileUploadConfiguration('myPropertyName'))
            ->setRequired()
            ->addValidator($mimeTypeValidator)
            ->addValidator($fileExtensionValidator)
            ->setMaxFiles(1)
            ->setUploadFolder('1:/user_upload/files/')
    );

    $this->arguments->getArgument('myArgument')->getPropertyMappingConfiguration()->skipProperties('myPropertyName');
}
Copied!

Configuration options for file uploads 

The configuration for a file upload is defined in a FileUploadConfiguration object.

This object contains the following configuration options.

Property name: 

Defines the name of the property of a domain model to which the file upload configuration applies. The value is automatically retrieved when using the FileUpload attribute. If the FileUploadConfiguration object is created manually, it must be set using the $propertyName constructor argument.

Validation: 

File upload validation is defined in an array of validators in the FileUploadConfiguration object.

The validators \TYPO3\CMS\Extbase\Validation\Validator\FileNameValidator , (ensures that no executable PHP files can be uploaded) and \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionMimeTypeConsistencyValidator (ensuring that the file extension matches the expected mime-type assumptions), are enforced and executed by default.

In addition, Extbase includes the following validators to validate an UploadedFile object:

  • \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\FileSizeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator

Those validators can either be configured with the FileUpload attribute or added manually to the configuration object with the addValidator method.

Required: 

Defines whether a file must be uploaded. If it is set to true, the minFiles configuration is set to 1.

Minimum files: 

Defines the minimum amount of files to be uploaded.

Maximum files: 

Defines the maximum amount of files to be uploaded.

Upload folder: 

Defines the upload path for the file upload. This configuration expects a storage identifier (e.g. 1:/user_upload/folder/). If the given target folder in the storage does not exist, it is created automatically.

Upload folder creation, when missing: 

The default creation of a missing storage folder can be disabled via the configuration attribute createUploadFolderIfNotExist ( bool, default true).

Add random suffix: 

When enabled, the filename of an uploaded and persisted file will contain a random 16 char suffix. As an example, an uploaded file named job-application.pdf will be persisted as job-application-<random-hash>.pdf in the upload folder.

The default value for this configuration is true and it is recommended to keep this configuration active.

This configuration only has an effect when uploaded files are persisted.

Duplication behavior: 

Defines the FAL behavior, when a file with the same name exists in the target folder. Possible values are DuplicationBehavior::RENAME (default), DuplicationBehavior::REPLACE and DuplicationBehavior::CANCEL.

Modifying existing configuration 

File upload configuration defined by the FileUpload attribute can be changed in the initialize*Action.

Example:

public function initializeCreateAction(): void
{
    $validator = GeneralUtility::makeInstance(MyCustomValidator::class);

    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurationForProperty('file');
    $configuration?->setMinFiles(2);
    $configuration?->addValidator($validator);
    $configuration?->setUploadFolder('1:/user_upload/custom_folder');
}
Copied!

The example shows how to modify the file upload configuration for the argument item and the property file. The minimum amount of files to be uploaded is set to 2 and a custom validator is added.

To remove all defined validators except the FileNameValidator, use the resetValidators() method.

Using TypoScript configuration for file uploads configuration 

When a file upload configuration for a property has been added using the FileUpload attribute, it may be required make the upload folder or other configuration options configurable with TypoScript.

Extension authors should use the initialize*Action to apply settings from TypoScript to a file upload configuration.

Example:

public function initializeCreateAction(): void
{
    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurationForProperty('file');
    $configuration?->setUploadFolder($this->settings['uploadFolder'] ?? '1:/fallback_folder');
}
Copied!

File upload validation 

Each uploaded file can be validated against a configurable set of validators. The validation section of the FileUpload attribute allows to configure commonly used validators using a configuration shorthand.

The following validation rules can be configured in the validation section of the FileUpload attribute:

  • required
  • minFiles
  • maxFiles
  • fileExtension (for \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator )
  • fileSize (for \TYPO3\CMS\Extbase\Validation\Validator\FilesizeValidator)
  • imageDimensions (for \TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator )
  • mimeType (for \TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator )
  • allowedMimeTypes (shorthand notation for configuration option allowedMimeTypes of the MimeTypeValidator)

Example:

#[FileUpload([
    'validation' => [
        'required' => true,
        'maxFiles' => 1,
        'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
        'mimeType' => ['allowedMimeTypes' => ['image/jpeg']],
        'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg']],
        'imageDimensions' => ['maxWidth' => 4096, 'maxHeight' => 4096]
    ],
    'uploadFolder' => '1:/user_upload/extbase_single_file/',
])]
Copied!

Extbase will internally use the Extbase file upload validators for fileExtensionMimeTypeConsistency, fileExtension, fileSize, mimeType and imageDimensions validation.

Custom validators can be created according to project requirements and must extend the Extbase AbstractValidator . The value to be validated is always a PSR-7 UploadedFile object. Custom validators can however not be used in the FileUpload attribute and must be configured manually.

Shorthand notation for allowedMimeTypes 

Using the mimeType configuration array, all options of the MimeTypeValidator can be set as sub-keys (since TYPO3 13.4.1):

#[FileUpload([
    'validation' => [
        'required' => true,
        'mimeType' => [
            'allowedMimeTypes' => ['image/jpeg'],
            'ignoreFileExtensionCheck' => false,
            'notAllowedMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.mimetype.notAllowedMessage',
            'invalidExtensionMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.mimetype.invalidExtensionMessage',
        ],
    ],
    'uploadFolder' => '1:/user_upload/files/',
])]
Copied!

The shorthand notation via 'allowedMimeTypes' continues to exist, in case only the mime type validation is needed. However, it is recommended to utilize the full 'mimeType' configuration array.

Deletion of uploaded files and file references 

The new Fluid ViewHelper Form.uploadDeleteCheckbox ViewHelper <f:form.uploadDeleteCheckbox> can be used to show a "delete file" checkbox in a form.

Example for object with FileReference property:

<f:form.uploadDeleteCheckbox property="file" fileReference="{object.file}" />
Copied!

Example for an object with an TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> property, containing multiple files and allowing to delete the first one (iteration is possible within Fluid, to do that for every object of the collection):

<f:form.uploadDeleteCheckbox property="file.0" fileReference="{object.file}" />
Copied!

Extbase will then handle file deletion(s) before persisting a validated object. It will:

  • validate that minimum and maximum file upload configuration for the affected property is fulfilled (only if the property has a FileUpload )
  • delete the affected sys_file_reference record
  • delete the affected file

Internally, Extbase uses FileUploadDeletionConfiguration objects to track file deletions for properties of arguments. Files are deleted directly without checking whether the current file is referenced by other objects.

Apart from using this ViewHelper, it is of course still possible to manipulate FileReference properties with custom logic before persistence.

New PSR-14 events 

The following new PSR-14 event has been added to allow customization of file upload related tasks:

ModifyUploadedFileTargetFilenameEvent 

The ModifyUploadedFileTargetFilenameEvent allows event listeners to alter a filename of an uploaded file before it is persisted.

Event listeners can use the method getTargetFilename() to retrieve the filename used for persistence of a configured uploaded file. The filename can then be adjusted via setTargetFilename(). The relevant configuration can be retrieved via getConfiguration().

Impact 

Extension developers can use the new feature to implement file uploads and file deletions in Extbase extensions easily with commonly known Extbase property attributes/annotations.

Feature: #103521 - Change table restrictions UI to combine read and write permissions 

See forge#103521

Description 

The tables_select and tables_modify fields of the be_groups table store information about permissions to read and write into selected database tables.

Due to TYPO3's internal behavior, when write permissions are granted for some tables, those tables are also automatically available for reading.

To make managing table permissions much easier and more efficient for integrators, the separate form fields for Tables (listing) [tables_select] and Tables (modify) [tables_modify] have been combined into a single UI element. This field now offers separate radio buttons to define which tables the backend user group should have permission to read and / or write. This is done by selecting one of the "No Access", "Read" or "Read & Write" options.

To further improve the user experience, it is also possible to use the "Check All", "Uncheck All" and "Toggle Selection" options for each permission.

Under the hood, when these permissions are processed, they are still saved separately in the tables_select and tables_modify columns in the be_groups table, as they were before.

To render this new table view and handle its behavior, a dedicated form renderType tablePermission has been introduced, which is now set for the tables_modify column. The tables_select column has been changed to TCA type passthrough.

The new form element is defined through: \TYPO3\CMS\Backend\Form\Element\TablePermissionElement . It uses a dedicated data provider defined in: \TYPO3\CMS\Backend\Form\FormDataProvider\TcaTablePermission . The JavaScript code is handled by a new web component: @typo3/backend/form-engine/element/table-permission-element.js.

When the TcaTablePermission data provider handles the configuration, it reads table lists from both the tables_select and tables_modify columns and combines them into a single array with unique table names.

Impact 

Managing table permissions for backend user groups has been improved by visually combining the Tables (listing) [tables_select] and Tables (modify) [tables_modify] options, as well as by adding the multi record selection functionality.

Feature: #103576 - Allow defining opacity in TCA type=color element 

See forge#103576

Description 

A new boolean property opacity has been added to the TCA configuration of a TCA type color element to allow defining colors with an opacity using the RRGGBBAA color notation.

'my_color' => [
    'label' => 'My Color',
    'config' => [
        'type' => 'color',
        'opacity' => true,
    ],
],
Copied!

Impact 

If opacity is enabled, editors can select not only a color but also its opacity in a corresponding color element.

Feature: #103581 - Automatically transform TCA field values for record objects 

See forge#103581

Description 

With forge#103783 the new \TYPO3\CMS\Core\Domain\Record object has been introduced. It is an object representing a raw database record, based on TCA and is usually used in the frontend (via Fluid Templates), when fetching records with the RecordTransformationProcessor ( record-transformation) or by collecting content elements with the PageContentFetchingProcessor ( page-content).

The Records API - introduced together with the Schema API in forge#104002 - now expands the record's values for most common field types (known from the TCA Schema) from their raw database value into "rich-flavored" values, which might be Record , FileReference , \TYPO3\CMS\Core\Resource\Folder or \DateTimeImmutable objects.

This works for the following "relation" TCA types:

  • category
  • file
  • folder
  • group
  • inline
  • select with MM and foreign_table

In addition, the values of following TCA types are also resolved and expanded automatically:

  • datetime
  • flex
  • json
  • link
  • select with a static list of entries

Each of the fields receives a full-fledged resolved value, based on the field configuration from TCA.

In case of relations ( category, group, inline, select with MM and foreign_table), a collection ( LazyRecordCollection) of new Record objects is attached as value. In case of file, a collection ( LazyFileReferenceCollection) of FileReference objects and in case of type folder, a collection ( LazyFolderCollection) of Folder objects are attached.

Example 

<f:for each="{myContent.main.records}" as="record">
    <f:for each="{record.image}" as="image">
        <f:image image="{image}" />
    </f:for>
</f:for>
Copied!

New TCA option relationship 

In order to define cardinality on TCA level, the option relationship is introduced for all "relation" TCA types listed above. If this option is set to oneToOne or manyToOne, then relations are resolved directly without being wrapped into collection objects. In case the relation can not be resolved, NULL is returned.

'image' => [
    'config' => [
        'type' => 'file',
        'relationship' => 'manyToOne',
    ]
]
Copied!
<f:for each="{myContent.main.records}" as="record">
    <f:image image="{record.image}" />
</f:for>
Copied!

Field expansion 

For TCA type flex, the corresponding FlexForm is resolved and therefore all values within this FlexForm are processed and expanded as well.

Fields of TCA type datetime will be transformed into a full \DateTimeInterface object.

Fields of TCA type json will provide the decoded JSON value.

Fields of TCA type link will provide the \TYPO3\CMS\Core\LinkHandling\TypolinkParameter object, which is an object oriented representation of the corresponding TypoLink parameter configuration.

Fields of TCA type select without a relationship will always provide an array of static values.

Impact 

When using Record objects through the \TYPO3\CMS\Core\Domain\RecordFactory API, e.g. via RecordTransformationProcessor ( record-transformation) or PageContentFetchingProcessor (page-content), the corresponding Record objects are now automatically processed and enriched.

Those can not only be used in the frontend but also for Backend Previews in the page module. This is possible by configuring a Fluid Template via Page TSconfig to be used for the page preview rendering:

mod.web_layout.tt_content.preview {
    textmedia = EXT:site/Resources/Private/Templates/Preview/Textmedia.html
}
Copied!

In such template the newly available variable {record} can be used to access the resolved field values. It is advised to migrate existing preview templates to this new object, as the former values will probably vanish in the next major version.

By utilizing the new API for fetching records and content elements, the need for further data processors, e.g. FilesProcessor ( files), becomes superfluous since all relations are resolved automatically when requested.

Feature: #103789 - Add "close"-button to page layout, if returnUrl is set 

See forge#103789

Description 

A "close"-button is now displayed in the page module, if the returnUrl argument is set. When this button is clicked, the previous module leading to the page module (or a custom link defined in returnUrl) will be displayed again.

In order to utilize this, backend module links set in extensions must pass the returnUrl argument. If returnUrl is not set, the "close"-button will not be displayed.

Examples 

Here is an example, using the Fluid <be:moduleLink> ViewHelper:

Fluid example
<a href="{be:moduleLink(route:'web_layout', arguments:'{id:pageUid, returnUrl: returnUrl}')}"
   class="btn btn-default"
   title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:title')}">
    <core:icon identifier="actions-document" size="small"/>
</a>
Copied!

The behaviour is similar to the <be:uri.editRecord> ViewHelper, where setting the returnUrl argument will also cause a "close"-button to be displayed.

The returnUrl should usually return to the calling (originating) module.

You can build the returnUrl with the Fluid ViewHelper be:uri:

Fluid example for building returnUrl to module "linkvalidator"
<f:be.uri route="web_linkvalidator" parameters="{id: pageUid}"/>
Copied!

Here is an example for building the returnUrl via PHP:

Backend module controller
use TYPO3\CMS\Backend\Routing\UriBuilder;

public function __construct(
    protected readonly UriBuilder $uriBuilder
) {}

protected function generateModuleUri(array $parameters = []): string
{
    return $this->uriBuilder->buildUriFromRoute('web_linkvalidator',  $parameters);
}

public function __invoke(ServerRequestInterface $request): ResponseInterface
{
    // ...
    $this->view->assign('returnUrl', $this->generateModuleUri(['pageUid' => $this->id]));
    // ...
}
Copied!

Impact 

The change has no impact, unless the functionality is being used. Extension authors can make use of the new functionality to also conveniently link back to an originating or custom module for a streamlined linear backend user-experience.

Feature: #104126 - Add configuration setting to define backend-locking file 

See forge#104126

Description 

TYPO3 supports the ability to lock the backend for maintenance reasons. This is controlled with a LOCK_BACKEND file that was previously stored in typo3conf/.

With Important: #104126 - Drop "typo3conf" directory from system status check and backend locking this directory is no longer needed, so now the location to this file can be adjusted via the new configuration setting $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] .

When empty, it falls back to a file LOCK_BACKEND, which is now stored by default in:

  • var/lock/ for Composer Mode
  • config/ for Legacy Mode

If you previously manually maintained the LOCK_BACKEND file (for example via deployment or other maintenance automation), please either adjust your automations to the new file location, or change the setting to the desired file location, or at best use the CLI commands vendor/bin/typo3 backend:lock and vendor/bin/typo3 backend:unlock.

The backend locking functionality is now contained in a distinct service class \TYPO3\CMS\Backend\Authentication\BackendLocker to allow future flexibility.

When upgrading an installation to Composer Mode with a locked backend in effect, please ensure your backend can remain locked by moving (or copying) the file to the new location var/lock/.

Remember, if you want locked backend state to persist between deployments, ensure that the used directory (var/lock by default) is shared between deployment releases.

Impact 

The location for LOCK_BACKEND to lock (and unlock) the backend can now be controlled by maintainers of a TYPO3 installation, and has moved outside of typo3conf/ by default to either var/lock/ (Composer) or config/ (Legacy).

Feature: #104168 - PSR-14 event for modifying countries 

See forge#104168

Description 

A new PSR-14 event \TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent has been introduced to modify the list of countries provided by \TYPO3\CMS\Core\Country\CountryProvider .

This event allows to add, remove and alter countries from the list used by the provider class itself and ViewHelpers like <f:form.countrySelect />.

Example 

An example corresponding event listener class:

<?php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent;

final readonly class EventListener
{
    #[AsEventListener(identifier: 'my-extension/before-countries-evaluated')]
    public function __invoke(BeforeCountriesEvaluatedEvent $event): void
    {
        $countries = $event->getCountries();
        unset($countries['BS']);
        $countries['XX'] = new Country(
            'XX',
            'XYZ',
            'Magic Kingdom',
            '987',
            '🔮',
            'Kingdom of Magic and Wonders'
        );
        $event->setCountries($countries);
    }
}
Copied!
EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']
    ['EXT:core/Resources/Private/Language/Iso/countries.xlf'][]
        = 'EXT:my_extension/Resources/Private/Language/countries.xlf';
Copied!
EXT:my_extension/Resources/Private/Language/countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

Impact 

Using the PSR-14 event BeforeCountriesEvaluatedEvent allows modification of countries provided by CountryProvider .

Feature: #104221 - PSR-14 events for RTE <-> Persistence transformations 

See forge#104221

Description 

When using an RTE HTML content element, two transformations take place within the TYPO3 backend:

  • From database: Fetching the current content from the database (persistence) and preparing it to be displayed inside the RTE HTML component.
  • To database: Retrieving the data returned by the RTE and preparing it to be persisted into the database.

This takes place in the \TYPO3\CMS\Core\Html\RteHtmlParser class, by utilizing the methods transformTextForRichTextEditor and transformTextForPersistence.

With forge#96107 and forge#92992, the former hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['transformation'] was removed, which took care of applying custom user-transformations. The suggested replacement for this was to use the actual RTE YAML configuration and API like allowAttributes.

Now, four PSR-14 Events are introduced to allow more granular control over data for persistence -> RTE and RTE -> persistence. This allows developers to apply more customized transformations, apart from the internal and API ones:

Modify data when saving RTE content to the database (persistence):

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent

Modify data when retrieving content from the database and pass to the RTE:

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent

All four events have the same structure (for now):

  • getHtmlContent() - retrieve the current HTML content
  • setHtmlContent() - used to set modifications of the HTML content
  • getInitialHtmlContent() - retrieve the untampered initial HTML content
  • getProcessingConfiguration() - retrieve processing configuration array

The event is meant to be used so that developers can change the HTML content either before the internal TYPO3 modifications, or after those.

The before events are executed before TYPO3 applied any kind of internal transformations, like for links. Event Listeners that want to modify output so that TYPO3 additionally operates on that, should listen to those before-Events.

When Event Listeners want to perform on the final result, the corresponding after-Events should be utilized.

Event listeners can use $value = $event->getHtmlContent() to get the current contents, apply changes to $value and then store the manipulated data via $event->setHtmlContent($value), see example:

Example 

An event listener class is constructed which will take an RTE input TYPO3 and internally store it in the database as [tag:typo3]. This could allow a content element data processor in the frontend to handle this part of the content with for example internal glossary operations.

The workflow would be:

  • Editor enters "TYPO3" in the RTE instance.
  • When saving, this gets stored as "[tag:typo3]".
  • When the editor sees the RTE instance again, "[tag:typo3]" gets replaced to "TYPO3" again.
  • So: The editor will always only see "TYPO3" and not know how it is internally handled.
  • The frontend output receives "[tag:typo3]" and could do its own content element magic, other services accessing the database could also use the parseable representation.

The corresponding event listener class:

EXT:MyExtension/Classes/EventListener/TransformListener.php
<?php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;

class TransformListener
{
    /**
     * Transforms the current value the RTE delivered into a value that is stored (persisted) in the database.
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-persistence')]
    public function modifyPersistence(AfterTransformTextForPersistenceEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('TYPO3', '[tag:typo3]', $value);
        $event->setHtmlContent($value);
    }

    /**
     * Transforms the current persisted value into something the RTE can display
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-richtexteditor')]
    public function modifyRichTextEditor(AfterTransformTextForRichTextEditorEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('[tag:typo3]', 'TYPO3', $value);
        $event->setHtmlContent($value);
    }
}
Copied!

Impact 

Using the new PSR-14 events

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent

allows to apply custom transformations for database <-> RTE content transformations.

Feature: #104311 - Auto created system TCA columns 

See forge#104311

Description 

Introduction 

There are various TCA table ctrl settings that define fields used to enable certain TYPO3 table capabilities and to specify the database column to store this row state.

An example is $GLOBALS['TCA']['ctrl']['enablecolumns']['starttime'] = 'starttime' , which makes the table "start time aware", resulting in the automatic exclusion of a record if the given start time is in the future, when rendered in the frontend.

Such ctrl settings require TCA columns definitions. Default definitions of such columns are now automatically added to TCA if not manually configured. Extension developers can now remove and avoid a significant amount of boilerplate field definitions in columns and rely on TYPO3 Core to create them automatically. Note the Core does not automatically add such columns to TCA types or palettes definitions: Developers still need to place them, to show the columns when editing record rows, and need to add according access permissions.

Let us have a quick look on what happened within TCA and its surrounding code lately, to see how this feature embeds within the general TYPO3 Core strategy in this area and why the above feature has been implemented at this point in time:

TCA has always been a central cornerstone of TYPO3. The TYPO3 Core strives to maintain this central part while simplifying and streamlining less desirable details.

TYPO3 version v12 aimed to simplify single column definitions by implementing new column types like file, category, email, and more. These are much easier to understand and require far fewer single property definitions than previous solutions. With this in place, auto-creation of database column definitions derived from TCA has been established with TYPO3 v13, making the manual definition of database table schemas in ext_tables.sql largely unnecessary. Additionally, an object-oriented approach called TcaSchema has been introduced to harmonize and simplify information retrieval from TCA.

With the step described in this document - the auto-creation of TCA columns from ctrl properties - the amount of manual boilerplate definitions is significantly reduced, and the TYPO3 Core gains more control over these columns to harmonize these fields throughout the system. Note that the TYPO3 Core has not yet altered the structure of TCA types and palettes. This will be one of the next steps in this area, but details have not been decided upon yet.

All these steps streamline TCA and its surrounding areas, simplify the system, and reduce the amount of details developers need to be aware of when defining their own tables and fields.

This document details the "column auto-creation from 'ctrl' fields" feature: It first lists all affected settings with their derived default definitions. It concludes with a section relevant for instances that still need to override certain defaults of these columns by explaining the order of files and classes involved in building TCA and the available options to change defaults and where to place these changes.

Auto-created columns from 'ctrl' 

The configuration settings below enable single table capabilities. Their values are a database column name responsible for storing the row data of the capability.

If a setting is defined in a "base" TCA table file (Configuration/TCA, not in Configuration/TCA/Overrides), the TYPO3 Core will add default columns definition for this field name if no definition exists in a base file.

$GLOBALS['TCA']['ctrl']['enablecolumns']['disabled'] 

This setting makes database table rows "disable aware": A row with this flag being set to 1 is not rendered in the frontend to casual website users.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'disabled' => 'disabled',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'disabled' => [
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
    'exclude' => true,
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
        'default' => 0,
        'items' => [
            [
                'label' => '',
                'invertStateDisplay' => true,
            ],
        ],
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['starttime'] 

This setting makes database table rows "starttime aware": A row having a start time in the future is not rendered in the frontend.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'starttime' => 'starttime',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'starttime' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
    'config' => [
        'type' => 'datetime',
        'default' => 0,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['endtime'] 

This setting makes database table rows "endtime aware": A row having an end time in the past is not rendered in the frontend.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'endtime' => 'endtime',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'endtime' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
    'config' => [
        'type' => 'datetime',
        'default' => 0,
        'range' => [
            'upper' => mktime(0, 0, 0, 1, 1, 2106),
        ],
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['fe_group'] 

This setting makes database table rows "frontend group aware": A row can be defined to be shown only to frontend users who are a member of selected groups.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'fe_group' => 'fe_group',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'fe_group' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.fe_group',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectMultipleSideBySide',
        'size' => 5,
        'maxitems' => 20,
        'items' => [
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hide_at_login',
                'value' => -1,
            ],
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.any_login',
                'value' => -2,
            ],
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.usergroups',
                'value' => '--div--',
            ],
        ],
        'exclusiveKeys' => '-1,-2',
        'foreign_table' => 'fe_groups',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['editlock'] 

This setting makes database table rows "backend lock aware": A row with this being flag enabled can only be edited by backend administrators.

Typical usage:

'ctrl' => [
    'editlock' => 'editlock',
],
Copied!

Default configuration added by the TYPO3 Core:

'endtime' => [
    'displayCond' => 'HIDE_FOR_NON_ADMINS',
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:editlock',
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['descriptionColumn'] 

This setting makes database table rows "description aware": Backend editors have a database field to add row specific notes.

Typical usage:

'ctrl' => [
    'descriptionColumn' => 'description',
],
Copied!

Default configuration added by the TYPO3 Core:

'description' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.description',
    'config' => [
        'type' => 'text',
        'rows' => 5,
        'cols' => 30,
        'max' => 2000,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['languageField'] and transOrigPointerField` 

These setting make database table rows "localization aware": Backend editors can create localized versions of a record. Note when languageField is set, and transOrigPointerField is not, the TYPO3 Core will automatically set transOrigPointerField to l10n_parent since both fields must be always set in combination.

Typical usage:

'ctrl' => [
    'languageField' => 'sys_language_uid',
    'transOrigPointerField' => 'l10n_parent',
],
Copied!

Default configuration added by the TYPO3 Core, note string $table corresponds to the current table name.

'sys_language_uid' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
    'config' => [
        'type' => 'language',
    ],
],
'l10n_parent' => [
    'displayCond' => 'FIELD:sys_language_uid:>:0',
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectSingle',
        'items' => [
            [
                'label' => '',
                'value' => 0,
            ],
        ],
        'foreign_table' => $table,
        'foreign_table_where' => 'AND {#' . $table . '}.{#pid}=###CURRENT_PID### AND {#' . $table . '}.{#' . $languageFieldName . '} IN (-1,0)',
        'default' => 0,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['transOrigDiffSourceField'] 

This setting makes database table rows "parent language record change aware": Backend editors can have an indicator when the parent column has been changed.

Typical usage:

'ctrl' => [
    'transOrigDiffSourceField' = 'l10n_diffsource',
],
Copied!

Default configuration added by the TYPO3 Core:

'l10n_diffsource' => [
    'config' => [
        'type' => 'passthrough',
        'default' => '',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['translationSource'] 

This setting makes database table rows "parent language source aware" to determine the difference between "connected mode" and "free mode".

Typical usage:

'ctrl' => [
    'translationSource' = 'l10n_source',
],
Copied!

Default configuration added by the TYPO3 Core:

'l10n_source' => [
    'config' => [
        'type' => 'passthrough',
        'default' => '',
    ],
],
Copied!

Load order when building TCA 

To understand if and when TCA column auto-creation from ctrl definitions kicks in, it is important to have an overview of the order of the single loading steps:

  1. Load single files from extension Configuration/TCA files
  2. NEW - Enrich columns from ctrl settings
  3. Load single files from extension Configuration/TCA/Overrides files
  4. Apply TCA migrations
  5. Apply TCA preparations

As a result of this strategy, columns fields are not auto-created, when a ctrl capability is added in a Configuration/TCA/Overrides file, and not in a Configuration/TCA "base" file. In general, such capabilities should be set in base files only: Adding them at a later point - for example in a different extension - is brittle and there is a risk the main extension can not deal with such an added capability properly.

Overriding definitions from auto-created TCA columns 

I most cases, developers do not need to change definitions of columns auto-created by the TYPO3 Core. In general, it is advisable to not actively do this. Developers who still want to change detail properties of such columns should generally stick to "display" related details only.

There are two options to have own definitions: When a column is already defined in a "base" TCA file (Configuration/TCA), the TYPO3 Core will not override it. Alternatively, a developer can decide to let the TYPO3 Core auto-create a column, to then override single properties in Configuration/TCA/Overrides files.

As example, "base" pages file defines this (step 1 above):

'ctrl' => [
    'enablecolumns' => [
        'disabled' => 'disabled',
    ],
],
Copied!

The TYPO3 Core thus creates this columns definition (step 2 above):

'columns' => [
    'disabled' => [
        'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
        'exclude' => true,
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
            'default' => 0,
            'items' => [
                [
                    'label' => '',
                    'invertStateDisplay' => true,
                ],
            ],
        ],
    ],
],
Copied!

When an editor creates a new page, it should be "disabled" by default to avoid having a new page online in the website before it is set up completely. A Configuration/TCA/Overrides/pages.php file does this:

<?php
// New pages are disabled by default
$GLOBALS['TCA']['pages']['columns']['hidden']['config']['default'] = 1;
Copied!

Impact 

Extension developers can typically remove columns definitions of all the above fields and rely on TYPO3 Core creating them with a good default definition.

It is only required to define the desired table capabilities in ctrl with its field names, and the system will create the according columns definitions automatically.

Feature: #104321 - Allow handling of argument mapping exceptions in ActionController 

See forge#104321

Description 

A new method handleArgumentMappingExceptions has been introduced in Extbase \TYPO3\CMS\Extbase\Mvc\Controller\ActionController to improve handling of exceptions that occur during argument mapping.

The new method supports optional handling of the following exceptions:

  • \TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException , which occurs, when a given object UID can not be resolved to an existing record.
  • \TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException , which occurs, when a required action argument is missing.

Handling of the exceptions can be enabled globally with the following TypoScript configuration.

  • config.tx_extbase.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • config.tx_extbase.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1

The exception handling can also be configured on extension level with the following TypoScript configuration.

  • plugin.tx_yourextension.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • plugin.tx_yourextension.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1
  • plugin.tx_yourextension_plugin1.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • plugin.tx_yourextension_plugin1.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1

By default, these options are set to 0, which will lead to exceptions being thrown (and would lead to errors, if not caught). This is the current behavior of TYPO3.

When setting one of these values to 1, the configured exceptions will not be thrown. Instead, a pageNotFound response is propagated, resulting in a 404 error being shown.

Additionally, extension authors can extend or override the method handleArgumentMappingExceptions in relevant Controllers in order to implement custom argument mapping exception handling.

Impact 

Extension authors can now handle exceptions in implementations of a ActionController , which are thrown during argument mapping.

Feature: #104451 - Redis backends support for key prefixing 

See forge#104451

Description 

It is now possible to add a dedicated key prefix for all invocations of a Redis cache or session backend. This allows to use the same Redis database for multiple caches or even for multiple TYPO3 instances if the provided prefix is unique.

Possible use cases are:

  • Using Redis caching for multiple caches, if only one Redis database is available
  • Pre-fill caches upon deployments using a new prefix (zero downtime deployments)
additional.php example for using Redis as session backend
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => '11',
        'compression' => true,
        'keyPrefix' => 'be_sessions_',
    ],
];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['FE'] = [
    'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => '11',
        'compression' => true,
        'keyPrefix' => 'fe_sessions_',
        'has_anonymous' => true,
    ],
];
Copied!
additional.php example for pages cache
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => 11,
        'compression' => true,
        'keyPrefix' => 'pages_';
    ],
];

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline'] = [
    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => 11,
        'compression' => true,
        'keyPrefix' => 'rootline_';
    ],
];
Copied!

Impact 

The new feature allows to use the same Redis database for multiple caches or even for multiple TYPO3 instances while having no impact on existing configuration.

Feature: #104482 - Add if() support to ExpressionBuilder 

See forge#104482

Description 

The TYPO3 \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder provides a new method to phrase "if-then-else" expressions. Those are translated into IF or CASE statements depending on the used database engine.

ExpressionBuilder::if() 

Creates an IF-THEN-ELSE expression.

Method signature
/**
 * Creates IF-THEN-ELSE expression construct compatible with all supported database vendors.
 * No automatic quoting or escaping is done, which allows to build up nested expression statements.
 *
 * **Example:**
 * ```
 * $queryBuilder
 *   ->selectLiteral(
 *     $queryBuilder->expr()->if(
 *       $queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
 *       $queryBuilder->quote('page-is-visible'),
 *       $queryBuilder->quote('page-is-not-visible'),
 *       'result_field_name'
 *     ),
 *   )
 *   ->from('pages');
 * ```
 *
 * **Result with MySQL:**
 * ```
 * SELECT (IF(`hidden` = 0, 'page-is-visible', 'page-is-not-visible')) AS `result_field_name` FROM `pages`
 * ```
 */
public function if(
    CompositeExpression|\Doctrine\DBAL\Query\Expression\CompositeExpression|\Stringable|string $condition,
    \Stringable|string $truePart,
    \Stringable|string $falsePart,
    \Stringable|string|null $as = null
): string {
    $platform = $this->connection->getDatabasePlatform();
    $pattern = match (true) {
        $platform instanceof DoctrineSQLitePlatform => 'IIF(%s, %s, %s)',
        $platform instanceof DoctrinePostgreSQLPlatform => 'CASE WHEN %s THEN %s ELSE %s END',
        $platform instanceof DoctrineMariaDBPlatform,
        $platform instanceof DoctrineMySQLPlatform => 'IF(%s, %s, %s)',
        default => throw new \RuntimeException(
            sprintf('Platform "%s" not supported for "%s"', $platform::class, __METHOD__),
            1721806463
        )
    };
    $expression = sprintf($pattern, $condition, $truePart, $falsePart);
    if ($as !== null) {
        $expression = $this->as(sprintf('(%s)', $expression), $as);
    }
    return $expression;
}
Copied!

Impact 

Extension authors can use the new expression method to build more advanced queries without the requirement to deal with the correct implementation for all supported database vendors.

Feature: #104493 - Add castText() expression support to ExpressionBuilder 

See forge#104493

Description 

The TYPO3 \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder provides a new method to cast expression results to text like datatypes. This is done to large VARCHAR/CHAR types using the CAST/CONVERT or similar methods based on the used database engine.

ExpressionBuilder::castText() 

Creates a CAST expression.

Method signature
/**
 * Creates a cast for the `$expression` result to a text datatype depending on the database management system.
 *
 * Note that for MySQL/MariaDB the corresponding CHAR/VARCHAR types are used with a length of `16383` reflecting
 * 65554 bytes with `utf8mb4` and working with default `max_packet_size=16KB`. For SQLite and PostgreSQL the text
 * type conversion is used.
 *
 * Main purpose of this expression is to use it in a expression chain to convert non-text values to text in chain
 * with other expressions, for example to {@see self::concat()} multiple values or to ensure the type,  within
 * `UNION/UNION ALL` query parts for example in recursive `Common Table Expressions` parts.
 *
 * This is a replacement for {@see QueryBuilder::castFieldToTextType()} with minor adjustments like enforcing and
 * limiting the size to a fixed variant to be more usable in sensible areas like `Common Table Expressions`.
 *
 * Alternatively the {@see self::castVarchar()} can be used which allows for dynamic length setting per expression
 * call.
 *
 * **Example:**
 * ```
 * $queryBuilder->expr()->castText(
 *    '(' . '1 * 10' . ')',
 *    'virtual_field'
 * );
 * ```
 *
 * **Result with MySQL:**
 * ```
 * CAST((1 * 10) AS CHAR(16383) AS `virtual_field`
 * ```
 *
 * @throws \RuntimeException when used with a unsupported platform.
 */
public function castText(CompositeExpression|\Stringable|string $expression, string $asIdentifier = ''): string
{
    $platform = $this->connection->getDatabasePlatform();
    if ($platform instanceof DoctrinePostgreSQLPlatform) {
        return $this->as(sprintf('((%s)::%s)', $expression, 'text'), $asIdentifier);
    }
    if ($platform instanceof DoctrineSQLitePlatform) {
        return $this->as(sprintf('(CAST((%s) AS %s))', $expression, 'TEXT'), $asIdentifier);
    }
    if ($platform instanceof DoctrineMariaDBPlatform) {
        // 16383 is the maximum for a VARCHAR field with `utf8mb4`
        return $this->as(sprintf('(CAST((%s) AS %s(%s)))', $expression, 'VARCHAR', '16383'), $asIdentifier);
    }
    if ($platform instanceof DoctrineMySQLPlatform) {
        // 16383 is the maximum for a VARCHAR field with `utf8mb4`
        return $this->as(sprintf('(CAST((%s) AS %s(%s)))', $expression, 'CHAR', '16383'), $asIdentifier);
    }
    throw new \RuntimeException(
        sprintf(
            '%s is not implemented for the used database platform "%s", yet!',
            __METHOD__,
            get_class($this->connection->getDatabasePlatform())
        ),
        1722105672
    );
}
Copied!

Impact 

Extension authors can use the new expression method to build more advanced queries without the requirement to deal with the correct implementation for all supported database vendors - at least to some grade.

Feature: #104526 - Provide validators for PSR-7 UploadedFile objects in Extbase 

See forge#104526

Description 

4 new Extbase validators have been added to allow common validation tasks of a PSR-7 \TYPO3\CMS\Core\Http\UploadedFile object or an \TYPO3\CMS\Extbase\Persistence\ObjectStorage containing PSR-7 UploadedFile objects.

Note, that the new validators can only be applied to the TYPO3 implementation of the PSR-7 \Psr\Http\Message\UploadedFileInterface because they validate the uploaded files before it has been moved.

Custom implementations of the \UploadedFileInterface must continue to implement their own validators.

FileNameValidator 

This validator ensures, that files with PHP executable file extensions can not be uploaded. The validator has no options.

FileSizeValidator 

This validator can be used to validate an uploaded file against a given minimum and maximum file size.

Validator options:

  • minimum - The minimum size as string (e.g. 100K)
  • maximum - The maximum size as string (e.g. 100K)

MimeTypeValidator 

This validator can be used to validate an uploaded file against a given set of accepted MIME types. The validator additionally verifies, that the given file extension of the uploaded file matches allowed file extensions for the detected mime type.

Validator options:

  • allowedMimeTypes - An array of allowed MIME types
  • ignoreFileExtensionCheck - If set to "true", it is checked, the file extension check is disabled

ImageDimensionsValidator 

This validator can be used to validate an uploaded image for given image dimensions. The validator must only be used, when it is ensured, that the uploaded file is an image (e.g. by validating the MIME type).

Validator options:

  • width - Fixed width of the image as integer
  • height - Fixed height of the image as integer
  • minWidth - Minimum width of the image as integer. Default is 0
  • maxWidth - Maximum width of the image as integer. Default is PHP_INT_MAX
  • minHeight - Minimum height of the image as integer. Default is 0
  • maxHeight - Maximum height of the image as integer. Default is PHP_INT_MAX

Impact 

TYPO3 extension autors can now use the new validators to validate a given UploadedFile object.

Feature: #104631 - Add UNION Clause support to the QueryBuilder 

See forge#104631

Description 

The UNION clause is used to combine the result sets of two or more SELECT statements, which all database vendors support, each with their own specific variations.

However, there is a commonly shared subset that works across all of them:

SELECT column_name(s) FROM table1
WHERE ...

UNION <ALL | DISTINCT>

SELECT column_name(s) FROM table2
WHERE ...

ORDER BY ...
LIMIT x OFFSET y
Copied!

with shared requirements:

  • Each SELECT must return the same fields in number, naming and order.
  • Each SELECT must not have ORDER BY, expect MySQL allowing it to be used as sub query expression encapsulated in parentheses.

Generic UNION clause support has been contributed to Doctrine DBAL and is included since Release 4.1.0 which introduces two new API method on the \QueryBuilder:

  • union(string|QueryBuilder $part) to create first UNION query part
  • addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT) to add additional UNION (ALL|DISTINCT) query parts with the selected union query type.

TYPO3 decorates the Doctrine DBAL \QueryBuilder to provide for most API methods automatic quoting of identifiers and values and to apply database restrictions automatically for SELECT queries.

The Doctrine DBAL API has been adopted now to provide the same surface for the TYPO3 \TYPO3\CMS\Core\Database\Query\QueryBuilder and the intermediate \TYPO3\CMS\Core\Database\Query\ConcreteQueryBuilder to make it easier to create UNION clause queries. The API on both methods allows to provide dedicated QueryBuilder instances or direct queries as strings in case it is needed.

In queries containing subqueries, only named placeholders (such as :username) can be used and must be registered on the outermost QueryBuilder object, similar to advanced query creation with SUB QUERIES.

UnionType::DISTINCT and UnionType::ALL 

Each subsequent part needs to be defined either as UNION DISTINCT or UNION ALL which could have not so obvious effects.

For example, using UNION ALL for all parts in between except for the last one would generate larger result sets first, but discards duplicates when adding the last result set. On the other side, using UNION ALL tells the query optimizer not to scan for duplicates and remove them at all which can be a performance improvement - if you can deal with duplicates it can be ensured that each part does not produce same outputs.

Example: Compose a UNION clause query 

Custom service class using a UNION query to retrieve data.
use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final readonly class MyService {
  public function __construct(
    private ConnectionPool $connectionPool,
  ) {}

  public function executeUnionQuery(
    int $pageIdOne,
    int $pageIdTwo,
  ): ?array {
    $connection = $this->connectionPool->getConnectionForTable('pages');
    $unionQueryBuilder = $connection->createQueryBuilder();
    $firstPartQueryBuilder = $connection->createQueryBuilder();
    $secondPartQueryBuilder = $connection->createQueryBuilder();
    // removing automatic TYPO3 restriction for the sake of the example
    // to match the PLAIN SQL example when executed. Not removing them
    // will generate corresponding restriction SQL code for each part.
    $firstPartQueryBuilder->getRestrictions()->removeAll();
    $secondPartQueryBuilder->getRestrictions()->removeAll();
    $expr = $unionQueryBuilder->expr();

    $firstPartQueryBuilder
      // The query parts **must** have the same column counts, and these
      // columns **must** have compatible types
      ->select('uid', 'pid', 'title')
      ->from('pages')
      ->where(
        $expr->eq(
          'pages.uid',
          // !!! Ensure to use most outer / top / main QueryBuilder
          //   instance for creating parameters and the complete
          //   query can be executed in the end.
          $unionQueryBuilder->createNamedParameter($pageIdOne, Connection::PARAM_INT),
        )
      );
    $secondPartQueryBuilder
      ->select('uid', 'pid', 'title')
      ->from('pages')
      ->where(
        $expr->eq(
          'pages.uid',
          // !!! Ensure to use most outer / top / main QueryBuilder instance
          $unionQueryBuilder->createNamedParameter($pageIdTwo, Connection::PARAM_INT),
        )
      );

    // Set first and second union part to the main (union)
    // QueryBuilder and return the retrieved rows.
    return $unionQueryBuilder
      ->union($firstPartQueryBuilder)
      ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
      ->orderBy('uid', 'ASC')
      ->executeQuery()
      ->fetchAllAssociative();
  }
}
Copied!

This would create the following query for MySQL with $pageIdOne = 100 and $pageIdTwo = 10:

    (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 100)
UNION
    (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 10)
ORDER BY `uid` ASC
Copied!

Impact 

Extension authors can use the new QueryBuilder methods to build more advanced queries.

Feature: #104655 - Add console command to mark upgrade wizards as undone 

See forge#104655

Description 

A new CLI command typo3 upgrade:mark:undone has been introduced. It allows to mark a previously executed upgrade wizard as "undone", so it can be run again.

This makes the existing functionality from the install tool also available on CLI.

Impact 

You can now mark an already executed upgrade wizard as "undone" with typo3 upgrade:mark:undone <wizardIdentifier>

Feature: #104773 - Generic view factory 

See forge#104773

Description 

Class \TYPO3\CMS\Core\View\ViewFactoryInterface has been added as a generic view interface to create views that return an instance of \TYPO3\CMS\Core\View\ViewInterface . This implements the "V" of "MVC" in a generic way and is used throughout the TYPO3 Core.

This obsoletes all custom view instance creation approaches within the TYPO3 Core and within TYPO3 extensions. Extensions should retrieve view instances based on this ViewFactoryInterface .

Impact 

Instances of this interface should be injected using dependency injection. The default injected implementation is a Fluid view, and can be reconfigured using dependency injection configuration, typically in a Services.yaml file.

A casual example to create and render a view looks like this.

use TYPO3\CMS\Core\View\ViewFactoryInterface;

class MyController
{
    public function __construct(
        private readonly ViewFactoryInterface $viewFactory,
    ) {}

    public function myAction(ServerRequestInterface $request): string
    {
        $viewFactoryData = new ViewFactoryData(
            templateRootPaths: ['EXT:myExt/Resources/Private/Templates'],
            partialRootPaths: ['EXT:myExt/Resources/Private/Partials'],
            layoutRootPaths: ['EXT:myExt/Resources/Private/Layouts'],
            request: $request,
        );
        $view = $this->viewFactory->create($viewFactoryData);
        $view->assign('myData', 'myData');
        return $view->render('path/to/template');
    }
}
Copied!

Note Extbase-based extensions create a view instance based on this factory by default and are accessible as $this->view.

Feature: #104789 - Support for contentArgumentName in AbstractViewHelper 

See forge#104789

Description 

ContentArgumentName has been a feature on Fluid ViewHelpers for some time now. It allows ViewHelpers to link a ViewHelper argument to the children of the ViewHelper name. As a result, an input value can either be specified as an argument or as the ViewHelper's children, leading to the same result.

Example:

<!-- Tag syntax -->
<f:format.json value="{data}" />
<f:format.json>{data}</f:format.json>

<!-- Inline syntax -->
{f:format.json(value: data)}
{data -> f:format.json()}
Copied!

Previously, this feature was only available to ViewHelpers using the trait \CompileWithContentArgumentAndRenderStatic . It is now available to all ViewHelpers since it has been integrated into the \AbstractViewHelper . The trait is no longer necessary.

To use the new feature, all the ViewHelper implementation needs to do is to define a method getContentArgumentName() which returns the name of the argument to be linked to the ViewHelper's children:

Example:

public function getContentArgumentName(): string
{
    return 'value';
}
Copied!

Impact 

ViewHelpers using the trait \CompileWithContentArgumentAndRenderStatic should be migrated to the new feature.

\CompileWithContentArgumentAndRenderStatic will continue to work in Fluid v4, but will log a deprecation level error message. It will be removed in Fluid v5.

Feature: #104794 - Introduce Site Settings Editor 

See forge#104794

Description 

A new Site Settings editor has been introduced that allows to configure per-site settings in file:config/sites/*/settings.yaml.

The new backend module Site Management > Settings provides an overview of sites which offer configurable settings and makes them editable based on Site Set provided Settings Definitions.

The editor shows a list of settings categories and respective settings. It will persist all settings into config/sites/*/settings.yaml. The module will only persist settings that deviate from the site-scoped default value. That means it will only change the minimal difference to the settings set defined by the active sets for the respective site.

The backend module is currently available for administrators only, but will likely be extended to be made available for editors in future.

Anonymous (undefined) site settings – as supported since TYPO3 v10 – will not be made editable, but will be preserved as-is when persisting changes through the settings editor.

Categorization 

Sets can define categories and settings definitions can reference the category by ID in order to assign a setting to a specific category. These definitions are placed in settings.definitions.yaml next to the site set file config.yaml.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
categories:
  myCategory:
    label: 'My Category'

settings:
  my.example.setting:
    label: 'My example setting'
    category: myCategory
    type: string
    default: ''

  my.seoRelevantSetting:
    label: 'My SEO relevant setting'
    # show in EXT:seo provided category "seo"
    category: seo
    type: int
    default: 5
Copied!

The settings ordering is defined through the loading order of extensions and by the order of categories. Uncategorized settings will be grouped into a virtual "Other" category and shown at the end of the list of available settings.

Readonly 

Site settings can be made readonly. They can be overridden only by editing the config/sites/my-site/settings.yaml but not from within the editor.

The value of the field is displayed in a readonly field in the settings editor.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  my.readonlySetting:
    label: 'My readonly setting'
    type: int
    default: 5
    readonly: true
Copied!

Enumeration of strings 

Site settings can provide possible options via the enum specifier, that will be selectable in the editor GUI:

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  my.enumSetting:
    label: 'My setting with options'
    type: string
    enum:
      valueA: 'Label of value A'
      valueB: 'Label of value B'
Copied!

Impact 

Site-scoped settings will most likely be the place to configure site-wide configuration, which was previously only possible to modify via Constant Editor, modifying TypoScript constants.

It is recommended to use site-sets and their UI configuration in favor of TypoScript Constants in the future.

Feature: #104814 - Automatically add system fields to content types 

See forge#104814

Description 

All content elements types ( CType) are usually equipped with the same system fields (language, hidden, etc.) - see also Feature: #104311 - Auto created system TCA columns. Adding them to the editor form has previously been done by adding those fields to each content types' showitem definition.

In the effort to simplify content element creation, to unify the available fields and position for the editor and to finally reduce configuration effort for integrators, those system fields are now added automatically based on the ctrl definition.

The following tabs / palettes are now added automatically:

  • The General tab with the general palette at the very beginning
  • The Language tab with the language palette after custom fields
  • The Access tab with the hidden and access palettes
  • The Notes tab with the rowDescription field

As mentioned, in case one of those palettes has been changed to no longer include the corresponding system fields, those fields are added individually depending on their definition in the table's ctrl section:

  • The ctrl[type] field (usually CType)
  • The colPos field
  • The ctrl[languageField] (usually sys_language_uid)
  • The ctrl[editlock] field (usually editlock)
  • The ctrl[enablecolumns][disabled] field (usually hidden)
  • The ctrl[enablecolumns][starttime] field (usually starttime)
  • The ctrl[enablecolumns][endtime] field (usually endtime)
  • The ctrl[enablecolumns][fe_group] field (usually fe_group)
  • The ctrl[descriptionColumn] field (usually rowDescription)

By default, all custom fields - the ones still defined in showitem - are added after the general palette and are therefore added to the General tab, unless a custom tab (e.g. Plugin, or Categories) is defined in between. It is also possible to start with a custom tab by defining a --div-- as the first item in the showitem. In this case, the General tab will be omitted.

All those system fields, which are added based on the ctrl section are also automatically removed from any custom palette and from the customized type's showitem definition.

If the content element defines the Extended tab, it will be inserted at the end, including all fields added to the type via API methods, without specifying a position, e.g. via ExtensionManagementUtility::addToAllTcaTypes().

Impact 

Creating content elements has been simplified by removing the need to define the system fields for each element again and again. This shrinks down a content element's showitem to just the element specific fields.

A usual migration will therefore look like the following:

Before:

'slider' => [
    'showitem' => '
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
            --palette--;;general,
            --palette--;;headers,
            slider_elements,
            bodytext;LLL:EXT:awesome_slider/Resources/Private/Language/locallang_ttc.xlf:bodytext.ALT.slider_description,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:appearance,
            --palette--;;frames,
            --palette--;;appearanceLinks,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
            --palette--;;language,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
            --palette--;;hidden,
            --palette--;;access,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
            categories,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes,
            rowDescription,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
    ',
],
Copied!

After:

'slider' => [
    'showitem' => '
            --palette--;;headers,
            slider_elements,
            bodytext;LLL:EXT:awesome_slider/Resources/Private/Language/locallang_ttc.xlf:bodytext.ALT.slider_description,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
            categories,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
    ',
],
Copied!

Since all fields, palettes and tabs, which are defined in the showitem are added after the general palette, also the Categories tab - if defined - is displayed before the system tabs / fields. The only special case is the Extended tab, which is always added at the end.

Feature: #104832 - PSR-14 Event to alter the results of PageTreeRepository 

See forge#104832

Description 

Until TYPO3 v9, it was possible to alter the rendering of one of TYPO3's superpowers — the page tree in the TYPO3 Backend User Interface.

This was done via a "Hook", but was removed due to the migration towards an SVG-based tree rendering.

As the Page Tree Rendering has evolved, and the hook system has been replaced in favor of PSR-14 Events, a new event \TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent has been introduced.

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, will remove any children for displaying for the page with the UID 123:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent;

final class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterRawPageRowPreparedEvent $event): void
    {
        $rawPage = $event->getRawPage();
        if ((int)$rawPage['uid'] === 123) {
            $rawPage['_children'] = [];
            $event->setRawPage($rawPage);
        }
    }
}
Copied!

Impact 

Using the new PSR-14 event, it is now possible to modify the populated page properties or its children records.

Feature: #104844 - Add widgets for listing all the sys_notes inside the TYPO3 system 

See forge#104844

Description 

To make it easier for TYPO3 users to view all the internal notes (EXT:sys_note) in their TYPO3 system, TYPO3 now offers dashboard widgets for each internal note type. The backend user must have access to the sys_note table and view permission to the page where the record is located.

Impact 

TYPO3 users who have access to the Dashboard module and are granted access to the new widgets can now add and use these widgets.

Feature: #104846 - Custom field transformations for new records 

See forge#104846

Description 

With forge#103783 the new \TYPO3\CMS\Core\Domain\Record object has been introduced, which is an object representing a raw database record based on TCA and is usually used in the frontend (via Fluid Templates).

Since Feature: #103581 - Automatically transform TCA field values for record objects the properties of those Record objects are transformed / expanded from their raw database value into "rich-flavored" values. Those values might be relations to e.g. Record , FileReference , Folder or \DateTimeImmutable objects.

However, TYPO3 does not know about custom field meanings, e.g. latitude and longitude information, stored in an input field or user settings stored as JSON in an TCA type json field. For such custom needs, the new PSR-14 \TYPO3\CMS\Core\Domain\Event\RecordCreationEvent has been introduced. It is dispatched right before a Record is created and therefore allows to fully manipulate any property, even the ones already transformed by TYPO3.

The new event is stoppable (implementing \StoppableEventInterface ), which allows listeners to actually create a Record object completely on their own.

The new event features the following methods:

  • setRecord() - Manually adds a RecordInterface object (stops the event propagation)
  • hasProperty() - Whether a property exists
  • setProperty() - Add or overwrite a property
  • setProperties() - Set properties for the RecordInterface
  • unsetProperty() - Unset a single property
  • getProperty() - Get the value for a single property
  • getProperties() - Get all properties
  • getRawRecord() - Get the RawRecord object
  • getSystemProperties() - Get the calculated SystemProperties
  • getContext() - Get the current Context (used to fetch the raw database row)
  • isPropagationStopped() - Whether the event propagation is stopped

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, creates a Coordinates object based on the field value of the coordinates field for the custom maps content type.

final class RecordCreationEventListener
{
    #[AsEventListener]
    public function __invoke(\TYPO3\CMS\Core\Domain\Event\RecordCreationEvent $event): void
    {
        $rawRecord = $event->getRawRecord();

        if ($rawRecord->getMainType() === 'tt_content' && $rawRecord->getRecordType() === 'maps' && $event->hasProperty('coordinates')) {
            $event->setProperty(
                'coordinates',
                new Coordinates($event->getProperty('coordinates'))
            );
        }
    }
}
Copied!

Impact 

Using the new PSR-14 RecordCreationEvent , extension authors are able to apply any field transformation to any property before a Record is created.

It is even possible to completely create a new RecordInterface object on their own.

Feature: #104868 - Add color scheme switching 

See forge#104868

Description 

Options have been added to switch between the available color schemes in TYPO3. A set of buttons for each available color scheme in the user dropdown at the top right and a setting in User Settings.

As the dark color scheme is currently regarded experimental until further notice, color scheme switching logic is currently hidden behind the UserTS setting setup.fields.colorScheme.disabled.

Impact 

It is now possible to switch to an automatic, light or dark color scheme for use in the backend.

Feature: #104878 - Introduce dashboard widget for pages with latest changes 

See forge#104878

Description 

To make it easier for TYPO3 users to view the latest changed pages in their TYPO3 system, TYPO3 now offers a dashboard widget that lists the latest changed pages.

Widget Options: - limit The limit of pages, displayed in the widget, default is 10 - historyLimit The maximum number of history records to check, default 1000

Impact 

TYPO3 users who have access to the Dashboard module and are granted access to the new widgets can now add and use this widget.

Feature: #104896 - Raise Fluid Standalone to 4.0 

See forge#104896

Description 

TYPO3 13 now uses Fluid 4 as the new base version. Old TYPO3 versions will keep using Fluid 2, which will still receive bugfixes if necessary. For detailed information about this release, please refer to the dedicated release notes on GitHub.

With the update to Fluid 4, tag-based ViewHelpers now have proper support for boolean attributes. Before this change, it was very cumbersome to generate these with Fluid, now it is implemented similar to popular JavaScript frameworks by using the newly introduced boolean literals:

<my:viewhelper async="{true}" />
Result: <tag async="async" />

<my:viewhelper async="{false}" />
Result: <tag />
Copied!

Of course, any variable containing a boolean can be supplied as well:

<my:viewhelper async="{isAsync}" />
Copied!

This can also be used in combination with variable casting:

<my:viewhelper async="{myString as boolean}" />
Copied!

For compatibility reasons empty strings still lead to the attribute being omitted from the tag.

Impact 

For existing installations, negative consequences of this update should be minimal as deprecated features will still work. Users are however advised to look into the already announced deprecations and to update their code accordingly. This update helps with this by now writing log messages to the deprecation log (if activated) if any deprecated feature is used in the TYPO3 instance.

Feature: #104904 - Ignore Fluid syntax error in <f:comment> 

See forge#104904

Description 

Fluid 4 brings a new template processor \TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\RemoveCommentsTemplateProcessor which removes Fluid comments created with the Debug ViewHelper <f:debug> from the template source string before the parsing process starts. It retains the original line breaks to ensure that error messages still refer to the correct line in the template.

By applying this template processor to all Fluid instances in the Core, it is now possible to use invalid Fluid code inside <f:comment> without triggering a Fluid error.

Impact 

This feature is helpful during template development because developers don't need to take care for commented-out code being valid Fluid code.

Feature: #104914 - Updated HTTP headers for frontend rendering and new TypoScript setting for proxies 

See forge#104914

Description 

In a typical frontend rendering scenario, TYPO3 sends HTTP response headers to deny caching to clients (= browsers) when e.g. a frontend user is logged in, a backend user is previewing a page, or a non-cacheable plugin is on a page.

When a frontend page is "client-cacheable", TYPO3 does not send any HTTP headers by default, but only when config.sendCacheHeaders = 1 is set via TypoScript.

In this case, TYPO3 sends the following HTTP Headers (example):

Expires: Thu, 26 Aug 2024 08:52:00 GMT
ETag: "d41d8cd98f00b204ecs00998ecf8427e"
Cache-Control: max-age=86400
Pragma: public
Copied!

However, in the past, this could lead to problems, because recurring website users might see outdated content for up to 24 hours (by default) or even longer, even if other website visitors already see new content, depending on various cache_timeout settings.

Thus, config.sendCacheHeaders = 1 should be used with extreme care.

However, this option was also used when a proxy / CDN / shared cache such as Varnish was put in between TYPO3 / the webserver and the client. The reverse proxy can then evaluate the HTTP Response Headers sent by TYPO3 frontend, put the TYPO3 response from the actual webserver into its "shared cache" and send a manipulated / adapted response to the client.

However, when working with proxies, it is much more helpful to take load off of TYPO3 / the webserver by keeping a cached version for a period of time and answering requests from the client, while still telling the client to not cache the response inside the browser cache.

This is now achieved with a new option config.sendCacheHeadersForSharedCaches = auto.

With this option enabled, TYPO3 now evaluates if the current TYPO3 frontend request is executed behind a Reverse Proxy, and if so, TYPO3 sends the following HTTP Response Headers at a cached response:

Expires: Thu, 26 Aug 2024 08:52:00 GMT ETag: "d41d8cd98f00b204ecs00998ecf8427e" Cache-Control: max-age=0, s-maxage=86400 Pragma: public

With config.sendCacheHeadersForSharedCaches = force the reverse proxy evaluation can be omitted, which can be used for local webserver internal caches.

"s-maxage" is a directive to tell shared caches - CDNs and reverse proxies - to keep a cached version of the HTTP response for a period of time (based on various cache settings) in their shared cache, while max-age=0 is evaluated at the client level. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details and if your reverse proxy supports this directive.

The new option takes precedence over config.sendCacheHeaders = 1 if running behind a reverse proxy.

Impact 

By utilizing the new TypoScript setting, TYPO3 caches cacheable contents, and also instructs shared caches such as reverse proxies or CDNs to cache the HTTP Response, while always delivering fresh content to the client, if certain routines for cache invalidation are in place. The latter is typically handled by TYPO3 extensions which hook into the cache invalidation process of TYPO3 to also invalidate cache entries in the reverse proxies.

In addition, compared to previous TYPO3 versions, client-cacheable HTTP Responses now send "Cache-Control: private, no-store" if no option applies.

Feature: #104935 - Allow moving content elements via page tree 

See forge#104935

Description 

To make managing content across pages easier, a backend user may now drag content elements from the Web > Page module into a page in the page tree.

Once dropped, a modal window opens, allowing the backend user to select the position for placing the content element and to change the target page if needed.

Impact 

Content elements can be moved from the Web > Page module into the page tree to initiate the moving process.

Feature: #104973 - Activate the shipped LintYaml executable for TYPO3 

See forge#104973

Description 

The typo3 executable received a new command lint:yaml to ease and encourage linting of YAML files before deploying to production and therefore avoid failures.

Usage as follows:

# Validates a single file
bin/typo3 lint:yaml path/to/file.yaml

# Validates multiple files
bin/typo3 lint:yaml path/to/file1.yaml path/to/file2.yaml

# Validates all files in a directory (also in sub-directories)
bin/typo3 lint:yaml path/to/directory

# Validates all files in multiple directories (also in sub-directories)
bin/typo3 lint:yaml path/to/directory1 path/to/directory2

# Exclude one or more files from linting
bin/typo3 lint:yaml path/to/directory --exclude=path/to/directory/foo.yaml --exclude=path/to/directory/bar.yaml

# Show help
bin/typo3 lint:yaml --help
Copied!

The help argument will list possible usage elements.

Impact 

Integrate easy made linting of YAML files from Core, custom extensions or any other source into your quality assurance workflow in the known format of the typo3 executable.

Feature: #104990 - Automatic frontend cache tagging 

See forge#104990

Description 

When database records are used in the frontend, and the rendered result is put into caches like the page cache, the TYPO3 frontend now automatically tags cache entries with lists of used records.

When changing such records in the backend, affected cache entries are dropped, leading to automatic cache eviction.

This is a huge improvement to previous TYPO3 versions where tagging and cache eviction had to configured manually.

This feature - automatically tagging cache entries - is the final solution to consistent caches at any point in time. It is however a bit tricky to get right in a performant way: There are still details to rule out, and the core will continue to improve in this area. The basic implementation in TYPO3 v13 however already resolves many use cases. Core development now goes ahead to see how this features behaves in the wild.

This feature is encapsulated in the feature toggle frontend.cache.autoTagging: It is enabled by default with new instances based on TYPO3 v13, and needs to be manually enabled for instances being upgrades from previous versions.

Impact 

Instances configured with the feature toggle automatically tag caches. Affected cache entries will be removed when changing records.

Deprecation: #101559 - Extbase uses ext:core ViewInterface 

See forge#101559

Description 

The default view of ext:extbase now returns a view that implements \TYPO3\CMS\Core\View\ViewInterface and not only \TYPO3Fluid\Fluid\View\ViewInterface anymore. This allows implementing any view that implements ViewInterface , and frees the direct dependency to Fluid.

The default return object is an instance of \TYPO3\CMS\Fluid\View\FluidViewAdapter which implements all special methods tailored for Fluid. Extbase controllers should check for instance of this object before calling these methods, especially:

  • getRenderingContext()
  • setRenderingContext()
  • renderSection()
  • renderPartial()

Method calls not being part of ViewInterface or the above listed method names have been marked as deprecated and will be removed in TYPO3 v14.

Impact 

Extbase controllers that extend ActionController and call methods not part of ViewInterface , should test for $view instanceof FluidViewAdapter before calling getRenderingContext(), setRenderingContext(), php:renderSection() and renderPartial().

All other Fluid related methods called on $view have been marked as deprecated and will log a deprecation level error message.

Affected installations 

Instances with Extbase based extensions that call $view methods without testing for FluidViewAdapter .

Migration 

Methods on "old" Fluid instances were wrapper methods for RenderingContext . Controllers should call $view->getRenderingContext() to perform operations instead.

Deprecation: #102422 - TypoScriptFrontendController->addCacheTags() and ->getPageCacheTags() 

See forge#102422

Description 

The methods \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() and \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags() have been marked as deprecated.

Impact 

Calling the methods \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() and \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags() will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations calling \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() or \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags().

Migration 

// Before
$GLOBALS['TSFE']->addCacheTags([
    'tx_myextension_mytable_123',
    'tx_myextension_mytable_456'
]);

// After
use TYPO3\CMS\Core\Cache\CacheTag;

$request->getAttribute('frontend.cache.collector')->addCacheTags(
    new CacheTag('tx_myextension_mytable_123', 3600),
    new CacheTag('tx_myextension_mytable_456', 3600)
);
Copied!
// Before
$GLOBALS['TSFE']->getPageCacheTags();

// After
$request->getAttribute('frontend.cache.collector')->getCacheTags();
Copied!

Deprecation: #102821 - ExtensionManagementUtility::addPItoST43() 

See forge#102821

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43() has been marked as deprecated in TYPO3 v13 and will be removed with TYPO3 v14.

Impact 

Using the ExtensionManagementUtility::addPItoST43() will raise a deprecation level log entry and a fatal error in TYPO3 v14.

Affected installations 

Extensions using ExtensionManagementUtility::addPItoST43() are affected: Using ExtensionManagementUtility::addPItoST43() triggers a deprecation level log message. The extension scanner will find usages of ExtensionManagementUtility::addPItoST43() as strong match.

Migration 

// Before:
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('my_extkey', '', '_pi1');
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript(
    'tx_myextkey',
    'setup',
    'plugin.tx_myextkey_pi1.userFunc = MY\MyExtkey\Plugins\Plugin->main'
);

// After:
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript(
    'my_extkey',
    'setup',
    'plugin.tx_myextkey_pi1 = USER_INT
     plugin.tx_myextkey_pi1.userFunc = MY\MyExtkey\Plugins\Plugin->main'
);
Copied!

Deprecation: #104223 - Fluid standalone methods 

See forge#104223

Description 

Some methods in Fluid standalone v2 have been marked as deprecated:

  • \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper->registerUniversalTagAttributes()
  • \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper->registerTagAttribute()

Impact 

Calling these methods is discouraged. They will log a deprecation level error when used with Fluid standalone v4.

Affected installations 

Instances with extensions calling above methods.

Migration 

registerUniversalTagAttributes() 

Within tag based ViewHelpers, calls to registerUniversalTagAttributes() should be removed. This method has been marked as @deprecated with Fluid standalone 2.12, will log a deprecation level error with Fluid standalone v4, and will be removed with v5.

When removing the call, attributes registered by the call are now available in $this->additionalArguments, and no longer in $this->arguments. This may need adaption within single ViewHelpers, if they handle such attributes on their own. For example, the common ViewHelper f:image was affected within the TYPO3 Core. The following attributes may need attention when removing registerUniversalTagAttributes(): class, dir, id, lang, style, title, accesskey, tabindex, onclick.

registerTagAttribute() 

Within tag based ViewHelpers, calls to registerTagAttribute() should be removed. This method has been marked as @deprecated with Fluid standalone 2.12, will log a deprecation level error with Fluid standalone v4, and will be removed with v5.

The call be often simply removed since arbitrary attributes not specifically registered are just added as-is by \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper . This only needs attention if single view helpers deal with such attributes within the render() method: When removing the call, those arguments are no longer available in $this->arguments, but in $this->additionalArguments. Additional attention is needed with attributes registered with type boolean: Those usually have some handling within render(). To stay compatible, it can be helpful to not simply remove the registerTagAttribute() call, but to turn it into a call to registerArgument().

Deprecation: #104304 - BackendUtility::getTcaFieldConfiguration 

See forge#104304

Description 

The method \TYPO3\CMS\Backend\Utility\BackendUtility::getTcaFieldConfiguration was introduced back in 2010 to add a simple abstraction to access "TCA" definitions of a field.

However, apart from the set up that it is not part of a flexible API without knowing the context, it was used seldom in TYPO3 Core.

The method has now been deprecated, as one could and can easily write the same PHP code with $GLOBALS['TCA'] in mind already (which the TYPO3 Core already did in several other places).

Now that Schema API was introduced, the last parts have been migrated to use the new API.

Impact 

Calling the PHP method BackendUtility::getTcaFieldConfiguration will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations with custom extensions using this method.

Migration 

Either access $GLOBALS['TCA'] directly (in order to support TYPO3 v12 and TYPO3 v13), or migrate to the new Schema API:

public function __construct(
    private readonly TcaSchemaFactory $tcaSchemaFactory
) {}

private function getFieldConfiguration(string $table, string $fieldName): array
{
    return $this->tcaSchemaFactory
        ->get($table)
        ->getField($fieldName)
        ->getConfiguration();
}
Copied!

Deprecation: #104325 - DiffUtility->makeDiffDisplay() 

See forge#104325

Description 

Method \TYPO3\CMS\Core\Utility\DiffUtility->makeDiffDisplay() and class property DiffUtility->stripTags have been deprecated in favor of new method DiffUtility->diff(). The new method no longer applies strip_tags() to the input strings.

This change makes class DiffUtility stateless: Property $stripTags will vanish in v14.

Impact 

Using method DiffUtility->makeDiffDisplay() will trigger a deprecation level error message.

Affected installations 

Instances with extensions calling DiffUtility->makeDiffDisplay().

Migration 

If DiffUtility->stripTags is not explicitly set to false, a typical migration looks like this:

// before
$diffUtility->DiffUtility->makeDiffDisplay($from, $to);

// after
$diffUtility->DiffUtility->diff(strip_tags($from), stripTags($to));
Copied!

If DiffUtility->stripTags = false is set before calling DiffUtility->makeDiffDisplay(), method diff() can be called as before, and DiffUtility->stripTags = false can be removed.

Deprecation: #104463 - Fluid standalone overrideArgument 

See forge#104463

Description 

Fluid standalone method \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper->overrideArgument() has been marked as deprecated.

Impact 

Using overrideArgument() in ViewHelpers logs a deprecation level error message in Fluid standalone v4, and will be removed with Fluid standalone v5. The method continues to work without deprecation level error message in Fluid standalone v2.

With Fluid standalone v2.14, registerArgument() no longer throws an exception if an argument is already registered. This allows to override existing arguments transparently without using overrideArgument().

Affected installations 

Instances with custom ViewHelpers using overrideArgument() are affected.

Migration 

Update typo3fluid/fluid to at least 2.14 and use registerArgument().

Deprecation: #104607 - BackendUserAuthentication:returnWebmounts() 

See forge#104607

Description 

Method \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::returnWebmounts() has been marked as deprecated and will be removed with TYPO3 v14.

Method BackendUserAuthentication::getWebmounts() was introduced as substitution. It returns a unique list of integer uids instead of a list of strings, which is more type safe. Superfluous calls to array_unique() can be removed since the uniqueness is now guaranteed by BackendUserAuthentication::getWebmounts().

Impact 

Calling BackendUserAuthentication::returnWebmounts() will trigger a PHP deprecation warning.

Affected installations 

All installations using BackendUserAuthentication::returnWebmounts() are affected.

Migration 

Existing calls to BackendUserAuthentication::returnWebmounts() should be replaced by BackendUserAuthentication::getWebmounts().

If third party extensions convert the previous result array from an array of strings to an array of integers, this can be skipped. In addition superfluous calls to array_unique() can be removed since the uniqueness is now guaranteed by BackendUserAuthentication::getWebmounts().

Deprecation: #104662 - BackendUtility thumbCode 

See forge#104662

Description 

The method \TYPO3\CMS\Backend\Utility\BackendUtility::thumbCode() has been deprecated since the method is no longer used in TYPO3 anymore. Additionally, due to multiple changes to file processing over the years, e.g. introducing of FAL, the method's signature changed a couple of times leading to a couple of method arguments are being unused, which is quite a bad API.

Impact 

Calling the PHP method BackendUtility::thumbCode() will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations with custom extensions using this method. The extension scanner will report any usage as strong match.

Migration 

Remove any usage of this method. In case you currently rely on the functionality, you can copy it to your custom extension. However, you might want to consider refactoring the corresponding code places.

The method basically resolved given FileReference objects. In case a file could not be resolved, a special icon has been rendered. Otherwise, the cropping configuration has been applied and the file's process() has been called to get the thumbnail, which has been wrapped in corresponding thumbnail markup. This might has been extended to also open the information modal on click.

This means the relevant parts are:

// Get file references
$fileReferences = BackendUtility:resolveFileReferences($table, $field, $row);

// Check for existence of the file
$fileReference->getOriginalFile()->isMissing()

// Render special icon if missing
$iconFactory
    ->getIcon('mimetypes-other-other', IconSize::MEDIUM, 'overlay-missing')
    ->setTitle(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') . ' ' . $fileObject->getName())
    ->render()

// Process file with cropping configuration if not missing
$fileReference->getOriginalFile()->process(
    ProcessedFile::CONTEXT_IMAGEPREVIEW,// ProcessedFile::CONTEXT_IMAGECROPSCALEMASK if cropArea is defiend
    [
        'width' => '...',
        'height' => '...',
        'crop' // If cropArea is defined
    ]
)

// Use cropped file and create <img> tag
<img src="' . $fileReference->getOriginalFile()->process()->getPublicUrl() . '"/>

// Wrap the info popup via <a> around the thumbnail
<a href="#" data-dispatch-action="TYPO3.InfoWindow.showItem" data-dispatch-args-list="_FILE,' . (int)$fileReference->getOriginalFile()->getUid() . '">
Copied!

Example of the HTML markup for a thumbnail:

<div class="preview-thumbnails" style="--preview-thumbnails-size: 64px">
    <div class="preview-thumbnails-element">
        <div class="preview-thumbnails-element-image">
            <img src="' . $fileReference->getOriginalFile()->process()->getPublicUrl() . '" width="64px" height="64px" alt="' . $fileReference->getAlternative() ?: $fileReference->getName()  . '" loading="lazy"/>
        </div>
    </div>
</div>
Copied!

Deprecation: #104684 - Fluid RenderingContext->getRequest() 

See forge#104684

Description 

The following methods have been marked as deprecated in TYPO3 v13 and will be removed with TYPO3 v14:

  • \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->setRequest()
  • \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->getRequest()

Impact 

Calling above methods triggers a deprecation level log entry in TYPO3 v13 and will trigger a fatal PHP error with TYPO3 v14.

Affected installations 

RenderingContext->getRequest() is a relatively common call in custom view helpers. Instances with extensions that deliver custom view helpers may be affected. The extension scanner is not configured to find potential places since the method names are common and would lead to too many false positives.

Migration 

Class \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext of the Core extension Fluid extends class \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext of Fluid standalone and adds the methods setRequest() and getRequest(). These methods are however not part of \TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface .

Fluid standalone will not add these methods, since the view of this library should stay free from direct PSR-7 \ServerRequestInterface dependencies. Having those methods in ext:fluid RenderingContext however collides with \RenderingContextInterface , which is type hinted in Fluid view helper method signatures.

Fluid standalone instead added three methods to handle arbitrary additional data in \RenderingContextInterface : setAttribute(), hasAttribute() and getAttribute(). Those should be used instead.

A typical usage in a view helper before:

/** @var \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext $renderingContext */
$renderingContext = $this->renderingContext;
$request = $renderingContext->getRequest();
Copied!

After:

// use Psr\Http\Message\ServerRequestInterface

$request = null;
if ($renderingContext->hasAttribute(ServerRequestInterface::class)) {
    $request = $renderingContext->getAttribute(ServerRequestInterface::class);
}
Copied!

To stay compatible to previous TYPO3 versions while avoiding deprecation notices, the following code can be used:

// use Psr\Http\Message\ServerRequestInterface

if (
    method_exists($renderingContext, 'getAttribute') &&
    method_exists($renderingContext, 'hasAttribute') &&
    $renderingContext->hasAttribute(ServerRequestInterface::class)
) {
    $request = $renderingContext->getAttribute(ServerRequestInterface::class);
} else {
    $request = $renderingContext->getRequest();
}
Copied!

Deprecation: #104764 - Fluid TemplatePaths->fillDefaultsByPackageName 

See forge#104764

Description 

Method \TYPO3\CMS\Fluid\View\TemplatePaths->fillDefaultsByPackageName() has been marked as deprecated in TYPO3 v13 and will be removed in TYPO3 v14.

Fluid template paths should be set directly using the methods setTemplateRootPaths(), setLayoutRootPaths() and setPartialRootPaths(), or - even better - be handled by ViewFactoryInterface, which comes as new feature in TYPO3 v13.

See Feature: #104773 - Generic view factory for more details of the generic view interface.

Impact 

Calling fillDefaultsByPackageName() triggers a deprecation level log level entry in TYPO3 v13 and will be removed in TYPO3 v14.

Affected installations 

The method is relatively rarely used by extensions directly, a usage in Extbase ActionController has been refactored away. The extension scanner will find candidates.

Note class TemplatePaths is marked @internal and should not be used by extensions at all.

Migration 

Use \TYPO3\CMS\Core\View\ViewFactoryInterface to create views with proper template paths instead. The TYPO3 system extensions come with plenty of examples on how to do this.

Deprecation: #104773 - Custom Fluid views and Extbase 

See forge#104773

Description 

These classes have been marked as deprecated in TYPO3 v13 and will be removed in v14:

  • \TYPO3\CMS\Fluid\View\StandaloneView
  • \TYPO3\CMS\Fluid\View\TemplateView
  • \TYPO3\CMS\Fluid\View\AbstractTemplateView
  • \TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface
  • \TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver

This change is related to the general View refactoring.

Impact 

Using one of the above classes triggers a deprecation level log entry.

Affected installations 

Instances with extensions that create view instances of \StandaloneView or \TemplateView are affected. The extension scanner will find possible candidates.

Migration 

Extensions should no longer directly instantiate own views, but should get \TYPO3\CMS\Core\View\ViewFactoryInterface injected and use create() to retrieve a view.

Within Extbase, ActionController->defaultViewObjectName should only be set to Extbase JsonView if needed, or not set at all. Custom view implementations should implement an own ViewFactoryInterface and configure controllers to inject an instance, or can set $this->defaultViewObjectName = JsonView::class in a custom __construct().

Deprecation: #104773 - ext:backend LoginProviderInterface changes 

See forge#104773

Description 

Method \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14.

Related to this, event \TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent has been changed to deprecate getController() and getPageRenderer(), while getRequest() has been added. getView() now typically returns an instance of ViewInterface.

This change is related to the general View refactoring.

Impact 

The default LoginProviderInterface implementation is UsernamePasswordLoginProvider provided by ext:core. This consumer has been adapted.

Using LoginProviderInterface->render() in TYPO3 v13 will trigger a deprecation level log entry and will fail in v14.

Affected installations 

Instances with custom login providers that change the TYPO3 backend login field rendering may be affected. The extension scanner is not configured to find usages, since method name render() is too common. A deprecation level log message is triggered upon use of the old method.

Migration 

Consumers of LoginProviderInterface should implement modifyView() instead, the transition should be smooth. Consumers that need the PageRenderer for JavaScript magic, should use dependency injection to receive an instance.

The default implementation in UsernamePasswordLoginProvider is a good example. Extensions that need to configure additional template, layout or partial lookup paths can extend them:

if ($view instanceof FluidViewAdapter) {
    $templatePaths = $view->getRenderingContext()->getTemplatePaths();
    $templateRootPaths = $templatePaths->getTemplateRootPaths();
    $templateRootPaths[] = 'EXT:my_extension/Resources/Private/Templates';
    $templatePaths->setTemplateRootPaths($templateRootPaths);
}
Copied!

Consumers of ModifyPageLayoutOnLoginProviderSelectionEvent should use the request instead, and/or should get an instance of PageRenderer injected as well.

Deprecation: #104778 - Instantiation of IconRegistry in ext_localconf.php 

See forge#104778

Description 

Since TYPO3 v11 it is possible to automatically register own icons via Configuration/Icons.php. Prior to this, extension authors used to register icons manually via instantiating the php:\TYPO3\CMS\Core\Imaging\IconRegistry in their ext_localconf.php files. This method has now been deprecated. It is recommended to switch to the newer method introduced with forge#94692.

Impact 

Instantiating IconRegistry inside ext_localconf.php files will trigger a deprecation-level log entry.

Affected installations 

All installations, which instantiate IconRegistry before the \TYPO3\CMS\Core\Core\Event\BootCompletedEvent . This includes ext_localconf.php files as well as TCA/Overrides.

Migration 

The most common use-cases can be accomplished via the Configuration/Icons.php file.

Before:

EXT:example/ext_localconf.php
<?php

$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
  \TYPO3\CMS\Core\Imaging\IconRegistry::class,
);
$iconRegistry->registerIcon(
    'example',
    \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
    [
        'source' => 'EXT:example/Resources/Public/Icons/example.svg'
    ],
);
Copied!

After:

EXT:example/Configuration/Icons.php
<?php

return [
    'example' => [
        'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
        'source' => 'EXT:example/Resources/Public/Icons/example.svg',
    ],
];
Copied!

For more complex tasks, it is recommended to register an event listener for the BootCompletedEvent . At this stage the system is fully booted and you have a completely configured IconRegistry at hand.

In case the registry was used in TCA/Overrides files to retrieve icon identifiers, then this should be replaced completely with static identifiers. The reason behind this is, that the registry isn't even fully usable at this stage. TCA isn't fully built yet and icons can still be registered at a later point.

Deprecation: #104789 - Fluid variables true, false, null 

See forge#104789

Description 

Fluid standalone will add proper language syntax for booleans and null with Fluid v4, which will be used in TYPO3 v13. Thus, user-defined variables named true, false and null are no longer allowed.

Impact 

Assigning variables with name true, false or null will throw an exception in Fluid v4. In preparation of this change, Fluid v2.15 logs a deprecation level error message if any of these variable names are used.

Affected installations 

Instances with Fluid templates using true, false or null as user-defined variable names. This should rarely happen, as it would involve using $view->assign('true', $someVar).

Migration 

Template code using these variables should be adjusted to use different variable names. In Fluid v4, the variables will contain their matching PHP counterparts.

Deprecation: #104789 - renderStatic() for Fluid ViewHelpers 

See forge#104789

Description 

The usage of renderStatic() for Fluid ViewHelpers has been deprecated. Also, Fluid standalone traits \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic and \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic have been marked as deprecated.

Impact 

Using one of the mentioned traits or renderStatic() in ViewHelpers logs a deprecation level error message in Fluid standalone v4. renderStatic() will no longer be called in Fluid standalone v5. renderStatic() and both traits continue to work without deprecation level error message in Fluid standalone v2.

Affected installations 

Instances with custom ViewHelpers using any variant of renderStatic() are affected.

Migration 

ViewHelpers should always use render() as their primary rendering method.

ViewHelpers using just renderStatic() without any trait or with the trait \CompileWithRenderStatic can be migrated by converting the static rendering method to a non-static method:

Before:

class MyViewHelper extends AbstractViewHelper
{
    use CompileWithRenderStatic;

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
    {
        return $renderChildrenClosure();
    }
}
Copied!

After:

class MyViewHelper extends AbstractViewHelper
{
    public function render(): string
    {
        return $this->renderChildren();
    }
}
Copied!

ViewHelpers using \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic can use the new contentArgumentName feature added with Fluid v2.15:

Before:

class MyViewHelper extends AbstractViewHelper
{
    use CompileWithContentArgumentAndRenderStatic;

    public function initializeArguments(): void
    {
        $this->registerArgument('value', 'string', 'a value');
    }

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
    {
        return $renderChildrenClosure();
    }

    public function resolveContentArgumentName(): string
    {
        return 'value';
    }
}
Copied!

After:

class MyViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument('value', 'string', 'a value');
    }

    public function render(): string
    {
        return $this->renderChildren();
    }

    public function getContentArgumentName(): string
    {
        return 'value';
    }
}
Copied!

Here is a basic recipe to perform this migration, preferably utilizing statical code analysis/replacement tools on your *ViewHelper.php files:

  • Find definitions of renderStatic
  • Rename method to render(), remove the arguments, remove static declaration
  • Within that method:

    • Replace $arguments with $this->arguments
    • Replace $renderingContext with $this->renderingContext
    • Replace $renderChildrenClosure() with $this->renderChildren()
    • Replace remaining $renderChildrenClosure usages with proper closure handling, like $this->renderChildren(...).
  • Replace resolveContentArgumentName( with getContentArgumentName(
  • Remove the mentioned definitions:

    • use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
    • use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
    • use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
    • use CompileWithRenderStatic; (class trait)
    • use CompileWithContentArgumentAndRenderStatic; (class trait)
  • (Optionally remove custom phpdoc annotations to the renderStatic parameters)
  • If you previously called ViewHelper's renderStatic methods in other places, you may utilize something like:

    $this->renderingContext->getViewHelperInvoker()->invoke(
        MyViewHelper::class,
        $arguments,
        $this->renderingContext,
        $this->renderChildren(...),
    );
    Copied!

Important: #101535 - Unused DB fields from tt_content removed 

See forge#101535

Description 

The database table tt_content contains all necessary fields for rendering content elements.

Back with TYPO3 v4.7, a major feature to render certain Content Types in a more accessible way, funded by the German Government (BLE_) with the "Konjunkturpaket II" was merged into CSS Styled Content.

In this procedure, certain Content Types received new fields and rendering definitions, which were stored in the database fields accessibility_title, accessibility_bypass and accessibility_bypass_text.

When CSS Styled Content was removed in favor of Fluid Styled Content in TYPO3 v8, the DB fields continued to exist in TYPO3 Core, so a migration from CSS Styled Content was possible.

However, the DB fields are not evaluated anymore since then, and are removed, along with their TCA definition in tt_content.

If these fields are still relevant for a custom legacy installation, these DB fields need to be re-created via TCA for further use in a third-party extension.

Important: #104126 - Drop "typo3conf" directory from system status check and backend locking 

See forge#104126

Description 

The directory typo3conf is no longer needed in Composer Mode. Checking for the existence of this directory is no longer performed in the Environment and Install Tool.

Previously it contained:

  • extensions (which are now Composer packages stored in vendor/),
  • the configuration files (which are now part of the config/ tree)
  • language labels and some artifact states (now part of var/)
  • a "backend lock" file (LOCK_BACKEND)

The location to this file can be adjusted via the new configuration setting $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] . See Feature: #104126 - Add configuration setting to define backend-locking file for details on this setting and location.

By default, LOCK_BACKEND is now located here:

  • var/lock/ for Composer Mode
  • config/ for Legacy Mode

13.2 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

Deprecation 

Important 

Feature: #91783 - Allow system maintainer to mute disable_functions error 

See forge#91783

Description 

Adds a configuration option to adapt the environment check in the Install Tool for a list of sanctioned disable_functions.

With the new configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions'] , a system maintainer can add native PHP function names to this list, which are then reported as environment warnings instead of errors.

Configuration example in additional.php:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions']
  = ['set_time_limit', 'set_file_buffer'];
Copied!

You can also define this in your settings.php file manually or via Admin Tools > Settings > Configure options.

Impact 

Native php function names can be added as an array of function names, which will not trigger an error but only a warning, if they can also be found in the php.ini setting disable_functions.

Feature: #92009 - Provide backend modules in LiveSearch 

See forge#92009

Description 

The backend LiveSearch is now capable of listing backend modules, a user has access to, offering the possibility of alternative navigation to different parts of the backend.

Impact 

Backend users now have another possibility to quickly access a backend module.

Feature: #99203 - Streamline FE/versionNumberInFilename to 'EXT:' resources 

See forge#99203

Description 

Local resources are currently not "cache-busted", for example, have no version in URL. TypoScript has no possibility to add the cache buster. When replacing them a new filename must be used (which feels little hacky).

getText "asset" to cache-bust assets in TypoScript 

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    value = { asset : EXT:core/Resources/Public/Icons/Extension.svg }
    insertData = 1
}
Copied!
Result
typo3/sysext/core/Resources/Public/Icons/Extension.svg?1709051481
Copied!

Cache-busted assets with the <f:uri.resource> ViewHelper 

EXT:my_extension/Resources/Private/Template/MyTemplate.html
<f:uri.resource
    path="EXT:core/Resources/Public/Icons/Extension.svg"
    useCacheBusting="true"
/>
Copied!
Comparison
Before: typo3/sysext/core/Resources/Public/Icons/Extension.svg
Now: typo3/sysext/core/Resources/Public/Icons/Extension.svg?1709051481
Copied!

The ViewHelper argument useCacheBusting is enabled by default.

Depending on $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] the cache buster is applied as query string or embedded in the filename.

Impact 

Local resources now can have a cache buster to easily replace them without changing the filename.

Feature: #102155 - User TSconfig option for default resources ViewMode 

See forge#102155

Description 

The listing of resources in the TYPO3 backend, e.g. in the File > Filelist module or the FileBrowser can be switched between list and tiles. TYPO3 serves tiles by default.

A new User TSconfig option options.defaultResourcesViewMode has been introduced, which allows the initial display mode to be defined. Valid values are therefore list and tiles, e.g.:

options.defaultResourcesViewMode = list
Copied!

Impact 

Integrators can now define the default display mode for resources via User TSconfig.

Feature: #102326 - Allow custom translations for Extbase validators 

See forge#102326

Description 

All validation messages from Extbase validators can now be overwritten using validator options. It is possible to provide either a translation key or a custom message as string.

Extbase validators providing only one validation message can be overwritten by a translation key or message using the validator option message. Validators providing multiple validation messages (e.g. Boolean, NotEmpty or NumberRange) use different validator options keys. In general, translation keys or messages for validators are registered in the validator property translationOptions.

Example with translations 

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'NotEmpty',
    'options' => [
        'nullMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.myProperty.notNull',
        'emptyMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.myProperty.notEmpty',
    ],
])]
protected string $myProperty = '';
Copied!

In this example, translation option keys for the NotEmptyValidator are overwritten for the property $myProperty. The locallang.xlf translation file from the extension my_extension will be used to lookup translations for the newly provided translation key options.

Example with a custom string 

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'Float',
    'options' => [
        'message' => 'A custom, non translatable message',
    ],
])]
protected float $myProperty = 0.0;
Copied!

In this example, translation option keys for the FloatValidator are overwritten for the property $myProperty. The message string is shown if validation fails.

Impact 

The new validator translation option keys allow developers to define unique validation messages for TYPO3 Extbase validators on validator usage basis. This may result in a better user experience, since validation messages now can refer to the current usage scope (e.g. "The field 'Title' is required" instead of "The given subject was empty.").

Feature: #102337 - PSR-14 event for modifying record list export data 

See forge#102337

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent has been introduced to modify the result of a download / export initiated in the Web > List module.

This replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , hooks, which have been deprecated.

The event allows body and header sections of the data dump to be modified, so that they can e.g. be used to redact specific data for GDPR compliance, transform / translate specific data, trigger creation of archives or web hooks, log export access and more.

The event offers the following methods:

  • getHeaderRow(): Return the current header row of the dataset.
  • setHeaderRow(): Sets the modified header row of the dataset.
  • getRecords(): Returns the current body rows of the dataset.
  • setRecords(): Sets the modified body rows of the dataset.
  • getRequest(): Returns the PSR request context.
  • getTable(): Returns the name of the database table of the dataset.
  • getFormat(): Returns the format of the download action (CSV/JSON).
  • getFilename(): Returns the name of the download filename (for browser output).
  • getId(): Returns the page UID of the download origin.
  • getModTSconfig(): Returns the active module TSconfig of the download origin.
  • getColumnsToRender(): Returns the list of header columns for the triggered download.
  • isHideTranslations(): Returns whether translations are hidden or not.

Example 

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace Vendor\MyPackage\Core\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/record-list-download-data')]
final readonly class DataListener
{
    public function __invoke(BeforeRecordDownloadIsExecutedEvent $event): void
    {
        // List of redactable fields.
        $gdprFields = ['title', 'author'];

        $headerRow = $event->getHeaderRow();
        $records = $event->getRecords();

        // Iterate header to mark redacted fields...
        foreach ($headerRow as $headerRowKey => $headerRowValue) {
            if (in_array($headerRowKey, $gdprFields, true)) {
                $headerRow[$headerRowKey] .= ' (REDACTED)';
            }
        }

        // Redact actual content...
        foreach ($records as $uid => $record) {
            foreach ($gdprFields as $gdprField) {
                if (isset($record[$gdprField])) {
                    $records[$uid][$gdprField] = '(REDACTED)';
                }
            }
        }

        $event->setHeaderRow($headerRow);
        $event->setRecords($records);
    }
}
Copied!

Migration 

The functionality of both hooks customizeCsvHeader and customizeCsvRow are now handled by the new PSR-14 event.

Migrating customizeCsvHeader 

The prior hook parameter/variable fields is now available via $event->getColumnsToRender(). The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getHeaderRow().

Migrating customizeCsvRow 

The prior hook parameters/variables have the following substitutes:

  • databaseRow is now available via $event->getRecords() (see note below).
  • tableName is now available via $event->getTable().
  • pageId is now available via $event->getId().

The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getRecords().

Please note that the hook was previously executed once per row retrieved from the database. The PSR-14 event however - due to performance reasons - is only executed for the full record list after database retrieval, thus allowing post-processing on the whole dataset.

Impact 

Using the PSR-14 event BeforeRecordDownloadIsExecutedEvent it is now possible to modify all of the data available when downloading / exporting a list of records via the Web > List module.

Feature: #102337 - PSR-14 event for modifying record list download presets 

See forge#102337

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent has been introduced to manipulate the list of available download presets in the Web > List module.

See Feature: #102337 - Presets for download of record lists for a detailed description of how to utilize presets when downloading a set of records from the backend in CSV or JSON format.

The event class offers the following methods:

  • getPresets(): Returns a list of presets set via TSconfig
  • setPresets(): Sets a modified list of presets.
  • getDatabaseTable(): Returns the database table name that a preset applies to.
  • getRequest(): Returns the PSR Request object for the context of the request.
  • getId(): Returns the page ID of the originating page.

Note that the event is dispatched for one specific database table. If an event listener is created to attach presets to different tables, the listener method must check for the table name, as shown in the example below.

If no download presets exist for a given table, the PSR-14 event can still be used to modify and add presets to it via the setPresets() method.

The array passed from getPresets() to setPresets() can contain an array collection of \TYPO3\CMS\Backend\RecordList\DownloadPreset objects with the array key using the preset label. The existing presets can be retrieved with these getters:

  • $preset->getLabel(): Name of the preset (can utilize LLL translations), optional.
  • $preset->getColumns(): Array of database table column names.
  • $preset->getIdentifier(): Identifier of the preset (manually set or calculated based on label and columns)

The event listener can also remove array indexes or columns of existing array entries by passing a newly constructed DownloadPreset object with the changed label and columns constructor properties.

Example 

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace Vendor\MyPackage\RecordList\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent;
use TYPO3\CMS\Backend\RecordList\DownloadPreset;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/modify-record-list-preset')]
final readonly class PresetListener
{
    public function __invoke(BeforeRecordDownloadPresetsAreDisplayedEvent $event): void
    {
        $presets = $event->getPresets();

        switch ($event->getDatabaseTable()) {
            case 'be_users':
                $presets[] = new DownloadPreset('PSR-14 preset', ['uid','email']);
                break;

            case 'pages':
                $presets[] = new DownloadPreset('PSR-14 preset', ['title']);
                $presets[] = new DownloadPreset('Another PSR-14 preset', ['title', 'doktype']);
                break;

            case 'tx_myvendor_myextension':
                $presets[] = new DownloadPreset('PSR-14 preset', ['uid', 'something']);
                break;
        }

        $presets[] = new DownloadPreset('Available everywhere, simple UID list', ['uid']);

        $presets['some-identifier'] = new DownloadPreset('Overwrite preset', ['uid, pid'], 'some-identifier');

        $event->setPresets($presets);
    }
}
Copied!

Impact 

Using the PSR-14 event BeforeRecordDownloadPresetsAreDisplayedEvent it is now possible to modify the presets of each table for downloading / exporting a list of such records via the Web > List module.

Feature: #102337 - Presets for download of record lists 

See forge#102337

Description 

In the Web > List backend module, the data of records for each database table (including pages and content records) can be downloaded.

This export takes the currently selected list of columns into consideration and alternatively allows all columns to be selected.

A new feature has been introduced adding the ability to pick the exported data columns from a list of configurable presets.

Those presets can be configured via page TSconfig, and can also be overridden via user TSconfig (for example, to make certain presets only available to specific users).

EXT:my_extension/Configuration/page.tsconfig
mod.web_list.downloadPresets {
    pages {
        10 {
            label = Quick overview
            columns = uid, title, crdate, slug
        }

        20 {
            identifier = LLL:EXT:myext/Resources/Private/Language/locallang.xlf:preset2.label
            label = UID and titles only
            columns = uid, title
        }
    }
}
Copied!

Each entry of mod.web_list.downloadPresets defines the table name on the first level (in this case pages), followed by any number of presets.

Each preset contains a label (the displayed name of the preset, which can be a locallang key), a comma-separated list of each column that should be included in the export as columns and optionally an identifier. If identifier is not provided, the identifier is generated as a hash of the label and columns.

This can be manipulated with user TSConfig by adding the page. prefix. User TSConfig is loaded after page TSConfig, so you can overwrite existing keys or replace the whole list of keys:

EXT:my_extension/Configuration/user.tsconfig
page.mod.web_list.downloadPresets {
    pages {
        10 {
            label = Quick overview (customized)
            columns = uid, title, crdate, slug
        }

        30 {
            label = Short with URL
            columns = uid, title, slug
        }
    }
}
Copied!

Since any table can be configured for a preset, any extension can deliver a defined set of presets through the EXT:my_extension/Configuration/page.tsconfig file and their table name(s).

Additionally, the list of presets can be manipulated via the new BeforeRecordDownloadPresetsAreDisplayedEvent.

Impact 

Editors can now export data with specific presets as required as identified by the website maintainer or extension developer(s).

It is no longer required to pick specific columns to export over and over again, and the list of presets is controlled by the website maintainer.

Two new PSR-14 Events have been added to allow further manipulation:

Feature: #102869 - List workspaces in LiveSearch 

See forge#102869

Description 

The backend LiveSearch is now able to list workspaces a backend user has access to, offering the possibility to switch to a workspace quickly outside the Workspaces module.

Impact 

Backend users now have another, quickly accessible way to access a workspace. With proper permissions, a backend user may also switch to the edit interface of a workspace to configure its settings.

Feature: #102951 - Provide PSR-7 request in Extbase validators 

See forge#102951

Description 

Extbase abstractValidator now provides a getter and a setter for the PSR-7 Request object. Validators extending AbstractValidator will include the PSR-7 request object, if the validator has been instantiated by Extbase ValidationResolver.

Impact 

Extension developers can now create custom validators which consume data from the PSR-7 request object (e.g. request attribute frontend.user).

Feature: #103019 - ModifyRedirectUrlValidationResultEvent PSR-14 event 

See forge#103019

Description 

This feature introduces the new PSR-14 event ModifyRedirectUrlValidationResultEvent in the felogin extension to provide developers the possibility and flexibility to implement custom validation for the redirect URL. This may be useful if TYPO3 frontend login acts as an SSO system or if users should be redirected to an external URL after login.

Example 

<?php

namespace Vendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent;

class ValidateRedirectUrl
{
    #[AsEventListener('validate-custom-redirect-url')]
    public function __invoke(ModifyRedirectUrlValidationResultEvent $event): void
    {
        $parsedUrl = parse_url($event->getRedirectUrl());
        if ($parsedUrl['host'] === 'trusted-host-for-redirect.tld') {
            $event->setValidationResult(true);
        }
    }
}
Copied!

Impact 

Developers now have the possibility to modify the validation results for the redirect URL, allowing redirects to URLs not matching existing validation constraints.

Feature: #103783 - RecordTransformation Data Processor 

See forge#103783

Description 

A new TypoScript data processor for FLUIDTEMPLATE and PAGEVIEW has been added.

The record-transformation Data Processor can typically be used in conjunction with the DatabaseQuery Data Processor. The DatabaseQuery Data Processor typically fetches records from the database, and the record-transformation will take the result and transform the objects into Record objects, which contain relevant data from the TCA table, which has been configured in the TCA columns fields for this record.

This is especially useful for TCA tables, which contain "types" (such as pages or tt_content database tables), where only relevant fields are added to the record object. In addition, special fields from "enableColumns" or deleted fields, as well as language and version information are extracted so they can be dealt with in a unified way.

The "type" property contains the database table name and the actual type based on the record, such as "tt_content.textmedia" for Content Elements.

Impact 

Example of the data processor being used in conjunction with DatabaseQuery processor.

page = PAGE
page {
  10 = PAGEVIEW
  10 {
    paths.10 = EXT:my_extension/Resources/Private/Templates/
    dataProcessing {
      10 = database-query
      10 {
        as = mainContent
        table = tt_content
        select.where = colPos=0
        dataProcessing.10 = record-transformation
      }
    }
  }
}
Copied!

Transform the current data array of FLUIDTEMPLATE to a Record object. This can be used for Content Elements of Fluid Styled Content or custom ones. In this example the FSC element "Text" has its data transformed for easier and enhanced usage.

tt_content.text {
  templateName = Text
  dataProcessing {
    10 = record-transformation
    10 {
      as = data
    }
  }
}
Copied!

Usage in Fluid templates 

The f:debug output of the Record object is misleading for integrators, as most properties are accessed differently than would be expected. The debug view is a better organized overview of all the available information. E.g. the property properties lists all relevant fields for the current Content Type.

We are dealing with an object here. You can, however, access your record properties as you are used to with {record.title} or {record.uid}. In addition, you gain special, context-aware properties like the language {record.languageId} or workspace {data.versionInfo.workspaceId}.

Overview of all possibilities:

<!-- Any property, which is available in the Record (like normal) -->
{record.title}
{record.uid}
{record.pid}

<!-- Language related properties -->
{record.languageId}
{record.languageInfo.translationParent}
{record.languageInfo.translationSource}

<!-- The overlaid uid -->
{record.overlaidUid}

<!-- Types are a combination of the table name and the Content Type name. -->
<!-- Example for table "tt_content" and CType "textpic": -->

<!-- "tt_content" (this is basically the table name) -->
{record.mainType}

<!-- "textpic" (this is the CType) -->
{record.recordType}

<!-- "tt_content.textpic" (Combination of mainType and record type, separated by a dot) -->
{record.fullType}

<!-- System related properties -->
{data.systemProperties.isDeleted}
{data.systemProperties.isDisabled}
{data.systemProperties.isLockedForEditing}
{data.systemProperties.createdAt}
{data.systemProperties.lastUpdatedAt}
{data.systemProperties.publishAt}
{data.systemProperties.publishUntil}
{data.systemProperties.userGroupRestriction}
{data.systemProperties.sorting}
{data.systemProperties.description}

<!-- Computed properties depending on the request context -->
{data.computedProperties.versionedUid}
{data.computedProperties.localizedUid}
{data.computedProperties.requestedOverlayLanguageId}
{data.computedProperties.translationSource} <!-- Only for pages, contains the Page model -->

<!-- Workspace related properties -->
{data.versionInfo.workspaceId}
{data.versionInfo.liveId}
{data.versionInfo.state.name}
{data.versionInfo.state.value}
{data.versionInfo.stageId}
Copied!

Available options 

The variable that contains the record(s) from a previous data processor,
or from a FLUIDTEMPLATE view. Default is :typoscript:`data`.
variableName = items

# the name of the database table of the records. Leave empty to auto-resolve
# the table from context.
table = tt_content

# the target variable where the resolved record objects are contained
# if empty, "record" or "records" (if multiple records are given) is used.
as = myRecords
Copied!

Feature: #103894 - Additional properties for columns in Page Layouts 

See forge#103894

Description 

Backend Layouts were introduced in TYPO3 v6 in order to customize the view of the Page module in TYPO3 backend for pages, but has since grown, also in frontend rendering, to select e.g. Fluid template files via TypoScript for a page, commonly used via data:pagelayout.

In order to use a single source for backend and frontend representation, the definition of a "Backend Layout" or "Page Layout" is expanded to also include more information for a specific content area. The Content Area was previously defined via "name" (for the label in the Page module) and "colPos", the numeric database field in which content is grouped in.

A definition can now optionally also contain a "slideMode" property and an "identifier" property next to each colPos, in order to simplify frontend rendering.

Whereas "identifier" is a speaking representation for the colPos, such as "main", "sidebar" or "footerArea", the "slideMode" can be set to one of the three options:

  • slideMode = slide - if no content is found, check the parent pages for more content
  • slideMode = collect - use all content from this page, and the parent pages as one collection
  • slideMode = collectReverse- same as "collect" but in the opposite order

With this information added, a new DataProcessor page-content ( \TYPO3\CMS\Frontend\DataProcessing\PageContentFetchingProcessor ) is introduced for the frontend rendering, which fetches all content for a page and respecting the settings from the page layout.

The new data processor allows to manipulate the fetched page content via the PSR-14 AfterContentHasBeenFetchedEvent.

Impact 

Enriching the backend layout information for each colPos enables a TYPO3 integrator to write less TypoScript in order to render content on a page.

The DataProcessor fetches all content elements from all defined columns with an included "identifier" in the selected backend layout and makes the resolved record objects available in the Fluid template via {content."myIdentifier".records}.

Example of an enriched backend layout definition:

mod.web_layout.BackendLayouts {
  default {
    title = Default
    config {
      backend_layout {
        colCount = 1
        rowCount = 1
        rows {
          1 {
            columns {
              1 {
                name = Main Content Area
                colPos = 0
                identifier = main
                slideMode = slide
              }
            }
          }
        }
      }
    }
  }
}
Copied!

Example of the frontend output:

page = PAGE
page.10 = PAGEVIEW
page.10.paths.10 = EXT:my_site_package/Tests/Resources/Private/Templates/
page.10.dataProcessing.10 = page-content
page.10.dataProcessing.10.as = myContent
Copied!
<main>
    <f:for each="{myContent.main.records}" as="record">
        <f:cObject typoscriptObjectPath="{record.mainType}" table="{record.mainType}" data="{record}"/>
    </f:for>
</main>
Copied!

The f:cObject ViewHelper above uses the rendering definition of the tt_content table {record.mainType} to render the Content Element from the list. The attribute data expects the raw database record, which is retrieved from {record}.

Feature: #104002 - Schema API 

See forge#104002

Description 

A new Schema API is introduced to access information about TCA structures in a unified way.

The main goal of this architecture is to reduce direct access to $GLOBALS['TCA'] after the Bootstrap process is completed.

The Schema API implements the following design goals:

  1. An object-oriented approach to access common TCA information such as if a database table is localizable or workspace-aware, if it has a "deleted" field ("soft-delete"), or other common functionality such as "enableFields" / "enablecolumns", which can be accessed via "Capabilities" within a Schema.
  2. A unified way to access information which "types" a TCA table has available, such as "tt_content", where the "CType" field is the divisor for types, thus, allowing a Schema to have sub-schemata for a TCA Table.

    The API in turn then handles which fields are available for a specific "CType". An example is "tt_content" with type "textpic": The sub-schema "tt_content.textpic" only contains the fields that are registered of that "CType", such as "bodytext", which then knows it is a Rich Text Field (the default column does not have this information), or "image" (a file relation field), but the sub-schema does not contain fields that are irrelevant for this type, such as "assets" (also a file relation field).

  3. An abstracted approach to available TCA field types such as "input" or "select", which also takes information into account, if a select field is a selection of a static list (such as "pages.layout") or if it contains a relation to another schema or field (based on "foreign_table"). Previously, this was evaluated in many places in TYPO3 Core, and can now be reduced.

    Thus, Schema API can now be utilized to determine the RelationshipType of a relational field type in a unified way without having to deal with deeply nested arrays.

  4. Information about relations to other database tables or fields. This is especially useful when dealing with Inline elements or category selection fields.

    Schema API can find out, which fields of other schemata are pointing to one-self. Schema API differentiates between an "Active Relation" and a "Passive Relation". An Active Relation is the information that a field such as "pages.media" (a field of type "file") contains a reference to the "sys_file_reference.uid_foreign" field. Active Relations in consequence are connected to a specific field (of type RelationalFieldTypeInterface).

    In turn, a "Passive Relation" is the information what other schemata/fields are pointing to a specific table or field.

    A common example of a "Passive Relation" is "sys_workspace_stage": The information stored in $GLOBALS[TCA][sys_workspace_stage] does not contain the information that this table is actually used as a reference from the database field sys_workspace.custom_stages, the sys_workspace_stage Schema now contains this information directly via TcaSchema->getPassiveRelations(). This is possible as TcaSchemaFactory is evaluating all TCA information and holistically as a graph. Passive Relations are currently only connected to a Schema, and Active Relations to a Field or a Schema.

    As the Schema API fetches information solely based on the TCA, an Active Relation only points to possible references, however, the actual reference (does a record really have a connection to another database table) would require an actual Record instance (a database row) to evaluate this information.

    Relations do not know about the "Type" or "Quantity" (many-to-many etc) as this information is already stored in the Field information. For this reason, the "Relations" currently only contain a flat information structure of the table (and possibly a field) pointing TO another schema name (Active Relation) or FROM another schema name / field (Passive Relation).

    Schema API also parses all available FlexForm data structures in order to resolve relations. As a result, a field of type FlexFormField contains a list of possible "FlexFormSchema" instances, which resolve all fields, sheets and section containers within each data structure.

  5. Once built, the Schema can never be changed. Whereas the TCA could be overridden at runtime, all TCA is now evaluated once and then cached. This is a consequence of working with an object-oriented approach.

    If the TCA is changed after the Bootstrap process is completed, the Schema needs to be rebuilt manually, which TYPO3 Core currently does, for example, in some Functional Testing Scenarios.

    All key objects (Schema, FieldType, Capabilities) are treated as immutable DTOs and never contain cross-references to their parent objects (Sub schemata do not know information about their parent schema, a field does not know which schema it belongs to), so the only entry point is always the TcaSchemaFactory object.

    This design allows the API to be fully cacheable at PHP level as a nested tree.

  6. Low-level, not full-fledged but serves as a basis.

    Several API decisions were made in order to let Schema API keep only its original purpose, but can be encapsulated further in other APIs:

    • Schema API is not available during Bootstrap as it needs TCA to be available and fully finished.
    • Schema API does not contain all available TCA properties for each field type. An example is "renderType" for select fields. This information is not relevant when querying records in the frontend, and mainly relevant for FormEngine - it is not generic enough to justify a getter method.
    • Extensibility: custom field types are currently not available until TYPO3 Core as fully migrated to Schema API.
    • User Permissions: Evaluating if a user has access to "tt_content.bodytext" requires information about the currently logged in user, thus it is not part of the Schema API. A "Permission API" should evaluate this information in the future.
    • Available options for a field. As an example, a common scenario is to find out which possible options are available for "pages.backend_layout". In TYPO3 Core an itemsProcFunc is connected to that field in TCA. Whether there is an itemsProcFunc is stored, but Schema API is not designed to actually execute the itemsProcFunc as it is dependent on various factors evaluated during runtime, such as the page it resides on, user permissions or PageTsConfig overrides.

    Schema API is currently marked as internal, as it might be changed during TYPO3 v13 development, because more parts of TYPO3 will be migrated towards Schema API.

    DataHandler and the Record Factory already utilize Schema API in order to reduce direct access to $GLOBALS[TCA].

    In the future Schema API might be used to evaluate information for Site Configurations, like TCA and FlexForms.

Impact 

Reading and writing $GLOBALS[TCA] within Configuration/TCA/* and via TCA Overrides is untouched, as the API is meant for reading the information there in a unified way.

Usage 

The API can now be used to find out information about TCA fields.

public function __construct(
    protected readonly PageRepository $pageRepository,
    protected readonly TcaSchemaFactory $tcaSchemaFactory
) {}

public function myMethod(string $tableName): void
{
    if (!$this->tcaSchemaFactory->has($tableName)) {
        // this table is not managed via TYPO3's TCA API
        return;
    }
    $schema = $this->tcaSchemaFactory->get($tableName);

    // Find out if a table is localizable
    if ($schema->isLocalizable()) {
        // do something
    }

    // Find all registered types
    $types = $schema->getSubSchemata();

}
Copied!

Using the API improves handling for parts such as evaluating columnsOverrides, foreign field structures, FlexForm Schema parsing, and evaluating type fields for database fields.

Feature: #104020 - ViewHelper to check feature flags 

See forge#104020

Description 

The <f:feature> ViewHelper allows integrators to check for feature flags from within Fluid templates. The ViewHelper follows the same rules as the underlying TYPO3 api, which means that undefined flags will be treated as false.

Examples 

Basic usage 

<f:feature name="myFeatureFlag">
   This is being shown if the flag is enabled
</f:feature>
Copied!

feature / then / else 

<f:feature name="myFeatureFlag">
   <f:then>
      Flag is enabled
   </f:then>
   <f:else>
      Flag is undefined or not enabled
   </f:else>
</f:feature>

Copied!

Impact 

Feature flags can now be checked from within Fluid templates.

Feature: #104067 - Sorting of forms in the form module 

See forge#104067

Description 

The listing of existing forms in the Form backend module has been extended to provide sorting functionality.

Impact 

It's now possible to sort the existing forms in the Form backend module.

Feature: #104069 - Improved backend notifications display and handling 

See forge#104069

Description 

The notifications shown on the lower right now have a "Clear all" button to allow the user to clear all notifications with a single click. This button is only displayed when two or more notifications are on screen.

In case the height of the notification container exceeds the viewport, a scroll bar will allow the user to navigate through the notifications.

Impact 

Handling of multiple notifications has been improved by allowing to scroll and clear all notifications at once.

Feature: #104085 - Edit specific columns of multiple records in List module 

See forge#104085

Description 

Using the "Show columns" button on a record table in the Web > List backend module allows to select the columns to be displayed for the corresponding table listing.

When selecting multiple records, it has already been possible to edit all those records at once, using the "Edit" button in the table header.

Now, a new button "Edit columns" has been introduced, which additionally allows to access the editing form for the selected records with just the columns of the current selection (based on "Show columns"). This improves the usability when doing mass editing of specific columns.

Impact 

It's now possible to edit the columns of multiple records in the Web > List backend module, using the new "Edit columns" button.

Feature: #104095 - Edit specific columns of multiple files in Filelist module 

See forge#104095

Description 

Using the "Show columns" action in the File > Filelist backend module allows to select the columns to be displayed file and folder listing.

When selecting multiple files, it has already been possible to edit the metadata of all those records at once, using the "Edit Metadata" button above the listing.

Now, a new button "Edit selected columns" has been introduced, which additionally allows to access the editing form for the selected files with just the columns of the current selection (based on "Show columns"). This improves the usability when doing mass editing of specific columns.

Impact 

It's now possible to edit selected columns of multiple file metadata in the File > Filelist backend module, using the new "Edit selected columns" button.

Feature: #104114 - Command to generate Fluid schema files 

See forge#104114

Description 

With Fluid Standalone 2.12, a new implementation of the XSD schema generator has been introduced, which was previously a separate composer package. These XSD files allow IDEs to provide autocompletion for ViewHelper arguments in Fluid templates, provided that they are included in the template by using the xmlns syntax:

<html
    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
    xmlns:my="http://typo3.org/ns/Vendor/MyPackage/ViewHelpers"
    data-namespace-typo3-fluid="true"
>
Copied!

A new CLI command has been defined to apply Fluid's new schema generator to TYPO3's Fluid integration. New Fluid APIs are used to find all ViewHelpers that exist in the current project (based on the composer autoloader). Then, TYPO3's configuration is checked for any merged Fluid namespaces (like f:, which consists of both Fluid Standalone and EXT:fluid ViewHelpers which in some cases override each other).

After that consolidation, *.xsd files are created in var/transient/ using another API from Fluid Standalone. These files which will automatically get picked up by supporting IDEs (like PhpStorm) to provide autocompletion in template files.

Impact 

To get autocompletion for all available ViewHelpers in supporting IDEs, the following CLI command can be executed in local development environments:

vendor/bin/typo3 fluid:schema:generate
Copied!

Feature: #104220 - Make parseFunc allowTags and denyTags optional 

See forge#104220

Description 

Defining the TypoScript properties allowTags or denyTags for the HTML processing via stdWrap.parseFunc is now optional.

Besides that, it is now possible to use allowTags = *.

Impact 

By omitting allowTags or denyTags, the corresponding rendering instructions can be simplified. Security aspects are considered automatically by the HTML sanitizer, unless htmlSanitize is disabled explicitly.

Examples 

10 = TEXT
10.value = <p><em>Example</em> <u>underlined</u> text</p>
10.parseFunc = 1
10.parseFunc {
  allowTags = *
  denyTags = u
}
Copied!

The example above allows any tag, except <u> which will be encoded.

10 = TEXT
10.value = <p><em>Example</em> <u>underlined</u> text</p>
10.parseFunc = 1
10.parseFunc {
  allowTags = u
}
Copied!

The example above only allows <u> and encodes any other tag.

10 = TEXT
10.value = <p><em>Example</em> <u>underlined</u> text</p>
10.parseFunc = 1
10.parseFunc {
  allowTags = *
  denyTags = *
}
Copied!

The example above allows all tags, the new allowTags = * takes precedence over denyTags = *.

Feature: #104223 - Update Fluid Standalone to 2.12 

See forge#104223

Description 

Fluid Standalone has been updated to version 2.12. This version adds new capabilities for tab based ViewHelpers and adds the new ViewHelper f:constant.

Also see this deprecation document for information on deprecated functionality.

Impact 

Arbitrary tags with tag based view helpers 

Tag based view helpers (such as <f:image /> or <f:form.*>) can now receive arbitrary tag attributes which will be appended to the resulting HTML tag, without dedicated registration.

<f:form.textfield inputmode="tel" />
<f:image image="{image}" hidden="hidden" />
Copied!

New f:constant ViewHelper 

A <f:constant> ViewHelper has been added to be able to access PHP constants from Fluid templates:

{f:constant(name: 'PHP_INT_MAX')}
{f:constant(name: '\Vendor\Package\Class::CONSTANT')}
{f:constant(name: '\Vendor\Package\Enum::CASE')}
Copied!

Deprecation: #102326 - RegularExpressionValidator validator option "errorMessage" 

See forge#102326

Description 

The errorMessage validator option provides a custom string as error message for validation failures of the RegularExpressionValidator. In order to streamline error message translation keys with other validators, the errorMessage validator option has been marked as deprecated in TYPO3 v13 and will be removed in TYPO3 v14.

Impact 

Using the errorMessage validator option with the RegularExpressionValidator will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations using the validator option errorMessage with the RegularExpressionValidator.

Migration 

The new message validator option should be used to provide a custom translatable error message for failed validation.

Before:

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'RegularExpression',
    'options' => [
        'regularExpression' => '/^simple[0-9]expression$/',
        'errorMessage' => 'Error message or LLL schema string',
    ],
])]
protected string $myProperty = '';
Copied!

After:

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'RegularExpression',
    'options' => [
        'regularExpression' => '/^simple[0-9]expression$/',
        'message' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my.languageKey'
    ],
])]
protected string $myProperty = '';
Copied!

Deprecation: #102337 - Deprecate hooks for record download 

See forge#102337

Description 

The previously used hooks $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , used to manipulate the download / export configuration of records, triggered in the Web > List backend module, have been deprecated in favor of a new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent .

Details for migration and functionality can be found in Feature: #102337 - PSR-14 event for modifying record list export data

Impact 

When the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] or $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] is executed, this will trigger a PHP deprecation warning.

The extension scanner will find possible usages with a weak match.

Affected installations 

All installations using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] or $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] are affected.

Migration 

The new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent can be used as a near drop-in replacement.

Deprecation: #103752 - Obsolete $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] 

See forge#103752

Description 

Configuration option $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] is obsolete and has been removed in TYPO3 Core v13.2.

This option is well-known to integrators who add relations to the TCA pages table. It triggers relation resolving of page relations for additional fields when rendering a frontend request in the default language. The most common usage is TypoScript "slide".

Impact 

Integrators can simply forget about this option: relations of table pages are now resolved with nearly no performance penalty in comparison to not having them resolved.

Affected installations 

Many instances add additional relations to the pages table then add this field in addRootLineFields. This option is no longer evaluated. Relation fields attached to pages are always resolved in frontend.

There should be hardly any extensions using this option, since it was an internal option of class RootlineUtility. Extensions using $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] may trigger a PHP warning level error because the array key has been removed. The extension scanner is configured to locate such usages.

Migration 

The option is no longer evaluated in TYPO3 Core. It is removed from settings.php during upgrade, if given.

Deprecation: #103785 - Deprecate MathUtility::convertToPositiveInteger() 

See forge#103785

Description 

TYPO3 has the method MathUtility::convertToPositiveInteger() to ensure that an integer is always positive. However, the method is rather "heavy" as it calls MathUtility::forceIntegerInRange() internally and therefore misuses a clamp mechanism to convert the integer to a positive number.

Also, the method name doesn't reflect what the method actually does. Negative numbers are not converted to their positive counterpart, but are swapped with 0. Due to the naming issue and the fact that the method can be replaced by a simple max() call, the method is therefore deprecated.

Impact 

Calling MathUtility::convertToPositiveInteger() will trigger a PHP deprecation warning.

Affected installations 

All installations using MathUtility::convertToPositiveInteger().

Migration 

To recover the original behavior of the deprecated method, its call can be replaced with max(0, $number). To actually convert negative numbers to their positive counterpart, call abs($number).

Deprecation: #103965 - Deprecate namespaced shorthand validator usage in Extbase 

See forge#103965

Description 

It is possible to use undocumented namespaced shorthand notation in Extbase to add validators to properties or arguments. For example, TYPO3.CMS.Extbase:NotEmpty will be resolved as \TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator and Vendor.Extension:Custom will be resolved as \Vendor\MyExtension\Validation\Validator\CustomValidator.

The namespaced shorthand notation for Extbase validators has been marked as deprecated and will be removed in TYPO3 v14.

Impact 

Using namespaced shorthand notation in Extbase will trigger a PHP deprecation warning.

Affected installations 

All installations using namespaced shorthand notation in Extbase.

Migration 

Extensions using the namespaced shorthand notation must use the FQCN of the validator instead. For Extbase core validators, the well known shorthand validator name can be used.

Before 

/**
 * @Extbase\Validate("TYPO3.CMS.Extbase:NotEmpty")
 */
protected $myProperty1;

/**
 * @Extbase\Validate("Vendor.Extension:Custom")
 */
protected $myProperty2;
Copied!

After 

/**
 * @Extbase\Validate("NotEmpty")
 */
protected $myProperty1;

/**
 * @Extbase\Validate("Vendor\Extension\Validation\Validator\CustomValidator")
 */
protected $myProperty2;
Copied!

or

#[Extbase\Validate(['validator' => \TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator::class])]
protected $myProperty1;

#[Extbase\Validate(['validator' => \Vendor\Extension\Validation\Validator\CustomValidator::class])]
protected $myProperty2;
Copied!

Deprecation: #104108 - Table dependant definition of columnsOnly 

See forge#104108

Description 

When linking to the edit form it's possible to instruct the EditDocumentController to only render a subset of available fields for relevant records using the columnsOnly functionality, by adding the fields to be rendered as a comma-separated list.

However, the edit form can render records from many tables, and not just a single table, in the same request.

Therefore, the limit fields functionality has been extended to allow setting the fields to be rendered on a per-table basis. This means that passing a comma-separated list of fields as a value for columnsOnly has been deprecated.

Impact 

Passing a comma-separated list of fields as value for columnsOnly will trigger a PHP deprecation warning. A compatibility layer will automatically set the field list for the required tables.

Affected installations 

All installations passing a comma-separated list of fields as a value for columnsOnly.

Migration 

The fields to be rendered have to be passed as an array under the corresponding table name.

An example, building such link using the UriBuilder:

$urlParameters = [
    'edit' => [
        'pages' => [
            1 => 'edit',
        ],
    ],
    'columnsOnly' => 'title,slug'
    'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
];

GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
Copied!

Above example has to be migrated to:

$urlParameters = [
    'edit' => [
        'pages' => [
            1 => 'edit',
        ],
    ],
    'columnsOnly' => [
        'pages' => [
            'title',
            'slug'
        ]
    ],
    'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
];

GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
Copied!

Additionally, when rendering records from many tables, a configuration could look like the following:

$urlParameters = [
    'edit' => [
        'pages' => [
            1 => 'edit',
        ],
        'tt_content' => [
            2 => 'edit',
        ],
    ],
    'columnsOnly' => [
        'pages' => [
            'title',
            'slug'
        ],
        'tt_content' => [
            'header',
            'subheader'
        ]
    ],
    'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
];

// https:://example.com/typo3/record/edit?edit[pages][1]=edit&edit[tt_content][2]=edit&columnsOnly[pages][0]=title&columnsOnly[pages][1]=slug&columnsOnly[tt_content][0]=header&columnsOnly[tt_content][1]=subheader
$link = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
Copied!

Deprecation: #104154 - Deprecate Utility.updateQueryStringParameter() 

See forge#104154

Description 

The Utility.updateQueryStringParameter() method in the @typo3/backend/utility.js module was introduced in TYPO3 v8 as a bugfix for highlighting in the old ExtJS-based page tree. Since removal of ExtJS in TYPO3 v9 the method has been unused.

Because safe removal of the method cannot be guaranteed as this point, it is therefore deprecated.

Impact 

Calling Utility.updateQueryStringParameter() will result in a JavaScript warning.

Affected installations 

All 3rd party extensions using the deprecated method.

Migration 

Now, JavaScript supports the URL and its related URLSearchParams object that can be used to achieve the same result:

const url = new URL('http://localhost?baz=baz');
url.searchParams.set('baz', 'bencer');
const urlString = url.toString(); // http://localhost?baz=bencer
Copied!

Important: #101621 - Changed default value for twitter_card field 

See forge#101621

Description 

The default value of the twitter_card field of a page is now an empty string instead of summary.

Meta tag <meta name="twitter:card"> is only rendered if one of the following fields is filled in,

  • twitter_title
  • twitter_description
  • twitter_image
  • twitter_card
  • og_title
  • og_description
  • og_image

If no twitter card is selected, the fallback value is summary.

Important: #103485 - Provide lib.parseFunc via ext:frontend 

See forge#103485

Description 

The lib.parseFunc and lib.parseFunc_RTE functions render HTML from Rich Text Fields in TYPO3. Direct interaction with these libraries is now uncommon, but they control the output of the <f:format.html> ViewHelper.

Previously, the libraries were available only through content rendering definitions like fluid_styled_content, the bootstrap_package or your own packages.

The <f:format.html> ViewHelper requires a parseFunc to function and will throw an exception if none is provided. With the shift towards self-contained content elements, also known as content blocks, there is no need to include a separate rendering definition. The frontend provides a base version of the libraries, which are now available in the frontend context.

The libraries are loaded early in the TypoScript chain, ensuring that all existing overrides continue to work as before, without the need for a basic parseFunc definition.

Important: #103748 - Reference index rebuild required 

See forge#103748

Description 

A series of new columns has been added to the reference index table sys_refindex. This requires a rebuild of the table. All instances must update the reference index when upgrading.

In TYPO3 v13 the reference index has become more important. Most notably, it is used in the frontend for performance improvements. This requires a valid index and keeping it up-to-date after deployments is mandatory to avoid incorrect data during frontend and backend processing.

After deployment and initial rebuild, the index is kept up-to-date automatically by the DataHandler when changing records in the backend .

In general, updating the reference index is required when database relations that are defined in TCA change - typically when adding, removing or changing extensions, and after TYPO3 Core updates (also patch level).

It is strongly recommended to update the reference index after deployments. Note TYPO3 v13 has optimized this operation - a full update is usually much quicker than with previous versions.

The recommended way to rebuild and fully update the reference index is the CLI command:

bin/typo3 referenceindex:update
Copied!

If CLI can not be used, the reference index can be updated in the backend using the "DB check" module in the "typo3/cms-lowlevel" extension. Since the update process may take a while, PHP web processes may time out during this operation, which makes this backend interface suitable for small sized instances only.

Important: #103915 - Adjust database field defaults for "check" TCA types 

See forge#103915

Description 

TYPO3 v13.0 has introduced automatic database field creation for TCA fields configured as type "check" (if not explicitly defined in ext_tables.sql), via https://review.typo3.org/c/Packages/TYPO3.CMS/+/80513.

This conversion applied a default 0 to all fields, and did not evaluate the actual TCA definition for the ['config']['default'] setting.

This bug has been fixed, and the DB schema analyzer will now convert all the following fields to their proper default settings:

  • be_users.options (0->3)
  • sys_file_storage.is_browsable (0->1)
  • sys_file_storage.is_writable (0->1)
  • sys_file_storage.is_online (0->1)
  • sys_file_storage.auto_extract_metadata (0->1)
  • sys_file_metadata.visible (0->1)
  • tt_content.sectionIndex (0->1)
  • tx_styleguide_palette.palette_1_1 (0->1)
  • tx_styleguide_palette.palette_1_3 (0->1)
  • tx_styleguide_valuesdefault.checkbox_1 (0->1)
  • tx_styleguide_valuesdefault.checkbox_2 (0->1)
  • tx_styleguide_valuesdefault.checkbox_3 (0->5)
  • tx_styleguide_valuesdefault.checkbox_4 (0->5)
  • sys_workspace.edit_allow_notificaton_settings (0->3)
  • sys_workspace.edit_notification_preselection (0->2)
  • sys_workspace.publish_allow_notificaton_settings (0->3)
  • sys_workspace.publish_notification_preselection (0->1)
  • sys_workspace.execute_allow_notificaton_settings (0->3)
  • sys_workspace.execute_notification_preselection (0->3)
  • sys_workspace_stage.allow_notificaton_settings (0->3)
  • sys_workspace_stage.notification_preselection (0->8)

All these records, created via DataHandler calls, actually evaluate the TCA default for record insertion and do not rely on SQL database field defaults.

Only records created using the QueryBuilder or other "raw" database calls would apply the wrong values.

An example of this is TYPO3\CMS\Core\Resource\StorageRepository->createLocalStorage() which creates a default fileadmin record via the QueryBuilder and then sets the field auto_extract_metadata to 0, instead of 1 as would be expected in the TCA. This would mean YouTube files would not automatically fetch metadata on creation.

This means, for all custom extension code that

  • removed the column definition in ext_tables.sql to enforce automatic database field creation,
  • and did not use the recommended DataHandler for record insertion (so, any code that is not executed in the backend context, using QueryBuilder or Extbase repository methods),
  • and expects a different default than 0 for newly created records,
  • and relied on the database field definition default

this code may have created incorrect database records for versions between TYPO3 v13.0 and 13.2.

For TYPO3 Core code, this has only affected:

  • Default file storage creation, field sys_file_metadata.auto_extract_metadata
  • Default backend user creation (admin) property be_users.options

In these rare case, the database record integrity needs to be checked manually, because there are no automated tools to see if a record has used SQL default values or specifically defined values.

Important: #104037 - Backend module "Access" renamed to "Permissions" 

See forge#104037

Description 

The TYPO3 backend module "Access" has been renamed to "Permissions".

This accurately reflects the purpose of this module and improves consistency in the TYPO3 backend.

Important: #104153 - About database error "Row size too large" 

See forge#104153

Description 

Introduction 

MySQL and MariaDB database engines sometimes generate a "Row size too large" error when modifying the schema of tables with many columns. This document aims to provide a detailed explanation of this error and presents solutions for TYPO3 instance maintainers to fix it.

Note that TYPO3 Core v13 has implemented measures to mitigate this error in most scenarios. Therefore, instance maintainers typically do not need to be aware of the specific details outlined below.

Preface 

Firstly, it is important to recognize that there are two different error messages that appear similar but have distinct root causes and potentially opposite solution strategies. This will be elaborated on later in this document.

Secondly, we will not cover all possible variations of these errors, but will focus on a subset most relevant to TYPO3. Therefore, later sections of the document are very specific. Correctly following the instructions may already resolve the issue for instances running a different setup.

The issue is most likely to occur with the database table tt_content, as this table is often extended with many additional columns, increasing the likelihood of encountering the error. This document uses table tt_content in code examples. However, the solution strategies are applicable to other tables as well by adjusting the code examples below.

Ensure storage engine is 'InnoDB' 

TYPO3 typically utilizes the InnoDB storage engine for tables in MySQL / MariaDB databases. However, instances upgraded from older TYPO3 Core versions might still employ different storage engines for some tables.

TYPO3 Core provides an automatic migration within Admin Tools > Maintenance > Analyze Database Structure and will suggest to migrate all tables to InnoDB.

You can manually verify the engine currently in use:

SELECT `TABLE_NAME`,`ENGINE`
FROM `information_schema`.`TABLES`
WHERE `TABLE_SCHEMA`='my_database'
AND `TABLE_NAME`='tt_content';
Copied!

Tables not using InnoDB should be converted via Admin Tools > Maintenance > Analyze Database Structure or manually via SQL:

USE `my_database`;
ALTER TABLE `tt_content` ENGINE=InnoDB;
Copied!

Ensure InnoDB row format is 'Dynamic' 

The InnoDB row format dictates how data is physically stored. The Dynamic row format provides better support for tables with many variable-length columns and has been the default format for some time. However, instances upgraded from older TYPO3 Core versions and older MySQL / MariaDB engines might still use the previous default format Compact.

TYPO3 Core provides an automatic migration within Admin Tools > Maintenance > Analyze Database Structure and will suggest to migrate all tables to ROW_FORMAT=DYNAMIC.

You can manually verify the row format currently in use:

SELECT `TABLE_NAME`,`Row_format`
FROM `information_schema`.`TABLES`
WHERE `TABLE_SCHEMA`='my_database'
AND `TABLE_NAME`='tt_content';
Copied!

Tables not using Dynamic should be converted via Admin Tools > Maintenance > Analyze Database Structure or manually via SQL:

USE 'my_database`;
ALTER TABLE `tt_content` ROW_FORMAT=DYNAMIC;
Copied!

Database, table and column charset 

The column charset impacts length calculations. This document assumes utf8mb4 for columns, aligning with the default TYPO3 setup. Converting an existing instance to utf8mb4 can be a complex task depending on the currently used charset and is beyond the scope of this document.

A key point about utf8mb4 is that when dealing with the utf8mb4 charset for VARCHAR() columns, storage and index calculations need to be multiplied by four (4). For example, a VARCHAR(20) can take up to eighty (80) bytes since each of the twenty (20) characters can use up to four (4) bytes. In contrast, a VARCHAR(20) in a latin1 column will consume only twenty (20) bytes, as each character is only one byte long.

The TYPO3 Core may set individual columns to a charset like latin1 in the future, which will optimize storage for ASCII-character only columns, but most content-related columns should be utf8mb4 to avoid issues with multi-byte characters.

Note that column types that do not store characters (like INT) do not have a charset. An overview of current charsets can be retrieved:

# Default charset of the database, new tables use this charset when no
# explicit charset is given with a "CREATE TABLE" statement:
SELECT `SCHEMA_NAME`, `DEFAULT_CHARACTER_SET_NAME` FROM `INFORMATION_SCHEMA`.`SCHEMATA`
WHERE `SCHEMA_NAME`='my_database';

# Default charset of a table, new columns use this charset when no
# explicit charset is given with a "ALTER TABLE" statement:
SELECT `table`.`table_name`,`charset`.`character_set_name`
FROM `information_schema`.`TABLES` AS `table`,`information_schema`.`COLLATION_CHARACTER_SET_APPLICABILITY` AS `charset`
WHERE `charset`.`collation_name`=`table`.`table_collation`
AND `table`.`table_schema`='my_database'
AND `table`.`table_name`='tt_content';

# List table columns, their column types with length and selected charsets:
SELECT `column_name`,`column_type`,`character_set_name`
FROM `information_schema`.`COLUMNS`
WHERE `table_schema`='my_database'
AND `table_name`='tt_content';
Copied!

Ensure innodb_page_size is 16384 

Few instances modify the MySQL / MariaDB innodb_page_size system variable, and it is advisable to keep the default value of 16384. Verify the current value:

SHOW variables WHERE `Variable_name`='innodb_page_size';
Copied!

Row size too large 

This document now assumes that MySQL / MariaDB is used, the table in question uses the InnoDB storage engine with Dynamic row format (please check Admin Tools > Maintenance > Analyze Database Structure which provides automatic migrations), innodb_page_size default 16384 is set, and that a system maintainer is aware of specific column charsets.

Error "Row size too large 65535" 

ERROR 1118 (42000): Row size too large. The maximum row size for the used table type,
not counting BLOBs, is 65535. This includes storage overhead, check the manual. You
have to change some columns to TEXT or BLOBs
Copied!

Explanation 

When altering the database schema of a table, such as adding or increasing the size of a VARCHAR column, the above error might occur.

Note the statement: "The maximum row size [...] is 65535".

MySQL / MariaDB impose a global maximum size per table row of 65kB. The combined length of all column types contribute to this limit, except for TEXT and BLOB types, which are stored "off row" where only a "pointer" to the actual storage location counts.

However, standard VARCHAR fields contribute their full maximum byte length towards this 65kB limit. For instance, a VARCHAR(2048) column with the utf8mb4 character set (4 bytes per character) requires 4 * 2048 = 8192 bytes. Therefore, only 65535 - 8192 = 57343 bytes remain available for the storage of all other table columns.

As another example, consider the query below which creates a table with a VARCHAR(16383) column alongside an INT column:

# ERROR 1118 (42000): Row size too large. The maximum row size [...] is 65535
CREATE TABLE test (c1 varchar(16383), c2 int) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Copied!

Let's break down the calculation:

varchar 16383 characters = 16383 * 4 bytes = 65532 bytes
int = 4 bytes
Total: 65532 + 4 = 65536 bytes
Copied!

This exceeds the maximum limit by one byte, causing the query to fail.

Mitigation 

The primary strategy to mitigate the 65kB limit is to minimize the use of lengthy VARCHAR columns.

For instance, in the tt_content table of a default Core instance, there are approximately a dozen VARCHAR(255) columns, totaling about 12kB, alongside smaller INT and similar fields. This leaves ample room for additional custom VARCHAR() columns.

TYPO3 v13 has introduced improvements in two key areas:

Firstly, TCA fields with type='link' and type='slug' have been converted from VARCHAR(2048) (requiring 8kB of row space) to TEXT. The tt_content table was affected by this change in at least one column ( header_link). This adjustment provides more space by default for custom columns.

Additionally, the TYPO3 Core now defaults to using TEXT instead of VARCHAR() for TCA fields with type='input' when the TCA property max is set to a value greater than 255 and extension authors utilize the column auto creation feature.

Instances encountering the 65kB limit can consider adjusting fields with these considerations in mind:

  • Priority should be given to reconsidering long VARCHAR() columns first. Changing a single utf8mb4 VARCHAR(2048) column to TEXT can free enough space for up to eight (8) utf8mb4 VARCHAR(255) columns.
  • Consider reducing the length of VARCHAR() columns. For instance, columns containing database table or column names can be limited to VARCHAR(64), as MySQL / MariaDB restricts table and column names to a maximum of 64 characters. Similar considerations apply to "short" content fields, such as a column storing an author's name or similar potentially limited length information.

    However, be cautious, as setting VARCHAR() columns to "too short" lengths may impose a different limit, as discussed below.

  • Consider removing entries from ext_tables.sql with TYPO3 Core v13: the column auto creation feature generally provides better-defined column definitions and ensures columns stay synchronized with TCA definitions automatically. The TYPO3 Core aims to provide sensible default definitions, often superior to a potentially imprecise definition by extension authors.
  • Note that individual column definitions in ext_tables.sql always override TYPO3 Core v13's column auto creation feature. In rare cases where TYPO3 Core's definition is inappropriate, extension authors can always override these details.
  • Note utf8mb4 VARCHAR(255) and TINYTEXT are not the same: a VARCHAR(255) size limit is 255 characters, while a TINYTEXT is 255 bytes. The proper substitution for a (4 bytes per character) utf8mb4 VARCHAR(255) field is TEXT, which allows for 65535 bytes.
  • TEXT may negatively impact performance as it forces additional Input/Output operations in the database. This is typically not a significant issue with standard TYPO3 queries, as various other operations in TYPO3 have a greater impact on overall performance. However, indiscriminately changing all fields from VARCHAR() to TEXT or similar is not advisable.
  • Be mindful of indices. When VARCHAR() columns that are part of an index are changed to TEXT or similar, these indexes may require adjustment. Ensure they are properly restricted in length to avoid a "Specified key was too long" error. The InnoDB key length limit with row format Dynamic is 3072 bytes (not characters). In general, indexes on VARCHAR() and all other "longish" columns should be set with care and only if really needed since long indexes can negatively impact database performance as well, especially when a table has many write operations in production.

Error "Row size too large (> 8126)" 

ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT
or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline.
Copied!

Sometimes there is also an error similar to this in MySQL / MariaDB logs:

[Warning] InnoDB: Cannot add field col1 in table db1.tab because after adding it,
the row size is 8478 which is greater than maximum allowed size (8126) for a record
on index leaf page.
Copied!

Explanation 

This error may occur when adding or updating table rows, not only when altering table schema.

Note the statement: "Row size too large (> 8126)". This differs from the previous error message. This error is not about a general row size limit of 65535 bytes, but a limit imposed by InnoDB tables.

The root cause is that InnoDB has a maximum row size equivalent to half of the innodb_page_size system variable value of 16384 bytes, which is 8192 bytes.

InnoDB mitigates this by storing certain variable-length columns on "overflow pages". The decision regarding which columns are actually stored on overflow pages is made dynamically when adding or changing rows. This is why the error can be raised at runtime and not only when altering the schema. Additionally, it makes accurately predicting whether the error will occur challenging. Furthermore, not all variable-length columns can be stored on overflow pages. This is why the error can be raised when altering table schema.

Variable-length columns of type TEXT and BLOB can always be stored on overflow pages, thus minimally impacting the main data page limit of 8192 bytes. However, VARCHAR columns can only be stored on overflow pages if their maximum length exceeds 255 bytes. Therefore, an unexpected solution to the "Row size too large 8192" error in many cases is to increase the length of some variable-length columns, enabling InnoDB to store them on overflow pages.

Mitigation 

TYPO3 Core v13 has modified several default columns to mitigate the issue for instances with many custom columns. The TYPO3 Core maintainers expect this issue to occur infrequently in practice.

Instances encountering the 8192 bytes limit can consider adjusting fields with these considerations in mind:

  • The calculation determining if a column can be stored on overflow pages is based on a minimum of 256 bytes, not characters. A typical utf8mb4 VARCHAR(255) equates to 1020 bytes, which can be stored on overflow pages. Changing such fields makes no difference.
  • Changing a utf8mb4 VARCHAR(63) (or smaller) to VARCHAR(64) (64 characters utf8mb4 = 256 bytes) allows this column to be stored on overflow pages and does make a difference.
  • Changing a utf8mb4 VARCHAR(63) (or smaller) to TINYTEXT should allow this column to be stored on overflow pages as well. However, this may not be the optimal solution due to potential performance penalties, as discussed earlier. Similarly, indiscriminately increasing the length of multiple variable-length columns is not advisable. Columns should ideally be kept as small as possible, only exceeding the 255-byte limit or converting to TEXT types if absolutely necessary. Also, refer to the note on indexes above when single columns are part of indexes.
  • Columns using utf8mb4 that are smaller or equal to VARCHAR(63) and only store ASCII characters can be downsized by changing the charset to latin1. For instance, a VARCHAR(60) column occupies 4 * 60 = 240 bytes in row size, but only 60 bytes when using the latin1 charset. Currently, TYPO3 Core does not interpret charset definitions for individual columns from ext_tables.sql. The Core Team anticipates implementing this feature in the future.
  • Note that increasing the length of VARCHAR columns can potentially conflict with the 65kB limit mentioned earlier. This is another reason to avoid indiscriminately increasing the length of variable-length columns.

Further reading 

This document is based on information from database vendors and other sites found online. The following links may provide further insights:

Final words 

Navigating the two limits in MySQL / MariaDB requires a deep understanding of database engine internals to manage them effectively. The TYPO3 Core Team is confident that version 13 has effectively mitigated the issue, ensuring that typical instances will rarely encounter it. We trust this document remains helpful and welcome any feedback in case something crucial has been overlooked.

13.1 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

Deprecation 

Important 

Feature: #93942 - Crop SVG images natively 

See forge#93942

Description 

Cropping SVG images via backend image editing or specific Fluid ViewHelper via <f:image> or <f:uri.image> (via crop attribute) now outputs native SVG files by default - which are processed but again stored as SVG, instead of rasterized PNG/JPG images like before.

Impact 

Editors and integrators can now crop SVG assets without an impact to their output quality.

Forced rasterization of cropped SVG assets can still be performed by setting the fileExtension="png" Fluid ViewHelper attribute or the TypoScript file.ext = png property.

<f:image> ViewHelper example: 

<f:image image="{image}" fileExtension="png" />
Copied!

This keeps forcing images to be generated as PNG image.

file.ext = png TypoScript example: 

page.10 = IMAGE
page.10.file = 2:/myfile.svg
page.10.file.crop = 20,20,500,500
page.10.file.ext = png
Copied!

If no special hard-coded option for the file extension is set, SVGs are now processed and stored as SVGs again.

Feature: #102836 - Allow deleting IRRE elements via postMessage() 

See forge#102836

Description 

To invoke a deletion on items in FormEngine's Inline Relation container API-wise, a new message identifier typo3:foreignRelation:delete has been introduced.

Example usage:

import { MessageUtility } from '@typo3/backend/utility/message-utility.js';

MessageUtility.send({
    actionName: 'typo3:foreignRelation:delete',
    objectGroup: 'data-<page_id>-<parent_table>-<parent_uid>-<reference_table>',
    uid: '<reference_uid>'
});
Copied!

Impact 

Extension developers are now able to trigger the deletion of IRRE elements via API.

Feature: #103043 - Modernize tree rendering and implement RTL and dark mode 

See forge#103043

Description 

The Tree feature in TYPO3 stands as one of its most iconic and widely used components, offering a visual representation of site structures to editors globally. Serving various purposes, such as file management, category/record selection, navigation, and more, the Tree has been a cornerstone for content handling.

Originally introduced in Version 8 with a performance-oriented approach, the SVG tree, powered by d3js, has faithfully served the community for the past seven years. While it excelled in providing a fast and efficient experience, it had its share of challenges, particularly due to its reliance on SVG and d3js.

Challenges with the SVG tree:

  • Limited functionality due to SVG constraints
  • Maintenance complexities with code-built SVG
  • Accessibility challenges
  • Difficulty in extension and innovation
  • Lack of native drag and drop
  • Complexity hindering understanding for many

Recognizing these challenges, we embarked on a journey to reimagine the Tree component, paving the way for a more adaptable and user-friendly experience.

Introducing the Modern Reactive Tree:

The new Tree, built on contemporary web standards, bids farewell to the SVG tree's limitations. Embracing native drag and drop APIs, standard HTML markup, and CSS for styling, the Modern Reactive Tree promises improved maintainability and accessibility.

Key enhancements:

  • Unified experience: All features are now consolidated into the base tree, ensuring a seamless and consistent user experience. This encompasses data loading and processing, selection, keyboard navigation, drag and drop, and basic node editing.
  • User preferences: The tree now dynamically adjusts to user preferences, supporting both light/dark mode and left-to-right (LTR) or right-to-left (RTL) writing modes.
  • Reactive rendering: Adopting a modern reactive rendering approach, the tree and its nodes now autonomously redraw themselves based on property changes, ensuring a smoother and more responsive interface.
  • Native drag and drop: Leveraging native drag and drop functionality opens up avenues for future enhancements, such as dragging content directly onto a page or seamlessly moving elements between browser windows.
  • Improved API endpoints: All endpoints delivering data for the tree now adhere to a defined API definition, enhancing consistency and compatibility with existing integrations.
  • Unified dragging tooltip handling: The dragging tooltip handling has been adjusted to a unified component that can be utilized across all components, ensuring synchronization across browser windows.
  • Dynamic tree status storage: The Pagetree status is no longer stored in the database. Instead, it is now stored in the local storage of the user's browser. This change empowers the browser to control the tree status, making it more convenient for users to transition between multiple browsers or machines.
  • Enhanced virtual scroll: The virtual scroll of the tree has been improved, ensuring that only nodes currently visible to the user are rendered to the DOM. Additionally, the focus on selected nodes is maintained even when scrolled out of view, providing a smoother and more user-friendly experience.

As we transition to this Modern Reactive Tree, we anticipate a renewed era of flexibility, ease of use, and potential for exciting future features.

Impact 

The TYPO3 CMS Tree modernization brings:

  • Personalization: Adapts to user preferences for a tailored interface.
  • Reactive design: Ensures smoother interactions.
  • Efficient integration: Improved API endpoints for seamless data exchange.
  • Consistency across devices: Unified dragging and dynamic storage for a consistent experience.
  • Enhanced performance: Optimal rendering during navigation.

These changes collectively enhance usability, adaptability, and performance, elevating the TYPO3 CMS Tree experience.

Feature: #103147 - Provide full userdata in password recovery email in ext:backend 

See forge#103147

Description 

A new array variable {userData} has been added to the password recovery FluidEmail object. It contains the values of all fields from the be_users table belonging to the affected backend user.

Impact 

It is now possible to use the {userData} variable in the password recovery FluidEmail to access data from the affected backend user.

Feature: #103186 - Introduce tree node status information 

See forge#103186

Description 

We've enhanced the backend tree component by extending tree nodes to incorporate status information. These details serve to indicate the status of nodes and provide supplementary information.

For instance, if a page undergoes changes within a workspace, it will now display an indicator on the respective tree node. Additionally, the status is appended to the node's title. This enhancement not only improves visual clarity but also enhances information accessibility.

Each node can accommodate multiple status information, prioritized by severity and urgency. Critical messages take precedence over other status notifications.

For example, status information can be added by using the event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent :

EXT:my_extension/Classes/Backend/EventListener/ModifyPageTreeItems.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-tree-items',
)]
final readonly class ModifyPageTreeItems
{
    public function __invoke(AfterPageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();
        foreach ($items as &$item) {
            if ($item['_page']['uid'] === 123) {
                $item['statusInformation'][] = new StatusInformation(
                    label: 'A warning message',
                    severity: ContextualFeedbackSeverity::WARNING,
                    priority: 0,
                    icon: 'actions-dot',
                    overlayIcon: '',
                );
            }
        }
        $event->setItems($items);
    }
}
Copied!

Impact 

Tree nodes can now have status information. Workspace changes are now reflected in the title of the node in addition to the indicator.

Feature: #103187 - Introduce CLI command to create backend user groups 

See forge#103187

Description 

A new CLI command ./bin/typo3 setup:begroups:default has been introduced as an alternative to the existing backend module. This command automates the creation of backend user groups, enabling the creation of two pre-configured backend user groups with permission presets applied.

Impact 

You can now use ./bin/typo3 setup:begroups:default to create pre-configured backend user groups without touching the GUI.

Example 

Interactive / guided setup (questions/answers):

Basic command
./bin/typo3 setup:begroups:default
Copied!

The backend user group can be set via the --groups|-g option. Allowed values for groups are Both, Editor and Advanced Editor:

Command examples
./bin/typo3 setup:begroups:default --groups Both
./bin/typo3 setup:begroups:default --groups Editor
./bin/typo3 setup:begroups:default --groups "Advanced Editor"
Copied!

When using the --no-interaction option, this defaults to Both.

Feature: #103211 - Introduce tree node labels 

See forge#103211

Description 

We've upgraded the backend tree component by extending tree nodes to incorporate labels, offering enhanced functionality and additional information.

Before the implementation of labels, developers and integrators relied on pageTree.backgroundColor.<pageid> for visual cues, which has been deprecated with TYPO3 v13. However, these background colors lacked accessibility and meaningful context, catering only to users with perfect eyesight and excluding those dependent on screen readers or contrast modes.

With labels, we now cater to all editors. These labels not only offer customizable color markings for tree nodes but also require an associated label for improved accessibility.

Each node can support multiple labels, sorted by priority, with the highest priority label taking precedence over others. Users can assign a label to a node via user TSconfig, noting that only one label can be set through this method.

EXT:my_extension/Configuration/user.tsconfig
options.pageTree.label.<pageid> {
    label = Campaign A
    color = #ff8700
}
Copied!

Labels also support locallang keys:

EXT:my_extension/Configuration/user.tsconfig
options.pageTree.label.<pageid> {
    label = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:labels.pageTree.campaign
    color = #ff8700
}
Copied!

The labels can also be added by using the event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent .

EXT:my_extension/Classes/Backend/EventListener/ModifyPageTreeItems.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-tree-items',
)]
final readonly class ModifyPageTreeItems
{
    public function __invoke(AfterPageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();
        foreach ($items as &$item) {
            // Add special label for all pages with parent page ID 123
            if (($item['_page']['pid'] ?? null) === 123) {
                $item['labels'][] = new Label(
                    label: 'Campaign B',
                    color: '#00658f',
                    priority: 1,
                );
            }
        }
        $event->setItems($items);
    }
}
Copied!

Please note that only the marker for the label with the highest priority is rendered. All additional labels will only be added to the title of the node.

Impact 

Labels are now added to the node and their children, significantly improving the clarity and accessibility of the tree component.

Feature: #103220 - Support comma-separated lists in page tree filter 

See forge#103220

Description 

The page tree has been enhanced to enable the user to not only search for strings and single page IDs, but for comma-separated lists of page IDs as well.

Feature: #103255 - Native support for language Scottish Gaelic added 

See forge#103255

Description 

TYPO3 now supports Scottish Gaelic. Scottish Gaelic language is spoken in Scotland.

The ISO 639-1 code for Scottish Gaelic is "gd", which is how TYPO3 accesses the language internally.

Impact 

It is now possible to

  • Fetch translated labels from translations.typo3.org / CrowdIn automatically within the TYPO3 backend.
  • Switch the backend interface to Scottish Gaelic language.
  • Create a new language in a site configuration using Scottish Gaelic.
  • Create translation files with the "gd" prefix (such as gd.locallang.xlf) to create your own labels.

TYPO3 will pick Scottish Gaelic as a language just like any other supported language.

Feature: #103309 - Add more expression methods to ExpressionBuilder 

See forge#103309

Description 

The TYPO3 \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder provides a relatively conservative set of database query expressions since a couple of TYPO3 and Doctrine DBAL versions now.

Additional expression methods are now available to build more advanced database queries that ensure compatibility across supported database vendors.

ExpressionBuilder::as() 

Creates a statement to append a field alias to a value, identifier or sub-expression.

Method signature
/**
 * @param string $expression Value, identifier or expression which
 *                           should be aliased.
 * @param string $asIdentifier Used to add a field identifier alias
 *                             (`AS`) if non-empty string (optional).
 *
 * @return string   Returns aliased expression.
 */
public function as(
    string $expression,
    string $asIdentifier = '',
): string {}

// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');
$expressionBuilder = $queryBuilder->expr();

$queryBuilder->selectLiteral(
  $queryBuilder->quoteIdentifier('uid'),
  $expressionBuilder->as('(1 + 1 + 1)', 'calculated_field'),
);

$queryBuilder->selectLiteral(
  $queryBuilder->quoteIdentifier('uid'),
  $expressionBuilder->as(
    $expressionBuilder->concat(
        $expressionBuilder->literal('1'),
        $expressionBuilder->literal(' '),
        $expressionBuilder->literal('1'),
    ),
    'concatenated_value'
  ),
);
Copied!

ExpressionBuilder::concat() 

Can be used to concatenate values, row field values or expression results into a single string value.

Method signature
/**
 * @param string ...$parts      Unquoted value or expression parts to
 *                              concatenate with each other
 * @return string  Returns the concatenation expression compatible with
 *                 the database connection platform.
 */
public function concat(string ...$parts): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('pages');
$expressionBuilder = $queryBuilder->expr();
$result = $queryBuilder
    ->select('uid', 'pid', 'title')
    ->addSelectLiteral(
        $expressionBuilder->concat(
            $queryBuilder->quoteIdentifier('title'),
            $queryBuilder->quote(' - ['),
            $queryBuilder->quoteIdentifier('uid'),
            $queryBuilder->quote('|'),
            $queryBuilder->quoteIdentifier('pid'),
            $queryBuilder->quote(']'),
        ) . ' AS ' . $queryBuilder->quoteIdentifier('page_title_info')
    )
    ->where(
        $expressionBuilder->eq(
            'pid',
            $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
        ),
    )
    ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // $row = array{
    //  'uid' => 1,
    //  'pid' => 0,
    //  'title' => 'Site Root Page',
    //  'page_title_info' => 'Site Root Page - [1|0]',
    // }
}
Copied!

ExpressionBuilder::castVarchar() 

Can be used to create an expression which converts a value, row field value or the result of an expression to varchar type with dynamic length.

Method signature
/**
 * @param string    $value          Unquoted value or expression,
 *                                  which should be casted.
 * @param int       $length         Dynamic varchar field length.
 * @param string    $asIdentifier   Used to add a field identifier alias
 *                                  (`AS`) if non-empty string (optional).
 * @return string   Returns the cast expression compatible for the database platform.
 */
public function castVarchar(
    string $value,
    int $length = 255,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$fieldVarcharCastExpression = $queryBuilder->expr()->castVarchar(
    $queryBuilder->quote('123'), // integer as string
    255,                         // convert to varchar(255) field - dynamic length
    'new_field_identifier',
);

$fieldExpressionCastExpression = $queryBuilder->expr()->castVarchar(
    '(100 + 200)',           // calculate a integer value
    100,                     // dynamic varchar(100) field
    'new_field_identifier',
);
Copied!

ExpressionBuilder::castInt() 

Can be used to create an expression which converts a value, row field value or the result of an expression to signed integer type.

Method signature
/**
 * @param string    $value         Quoted value or expression result which
 *                                 should be casted to integer type.
 * @param string    $asIdentifier  Used to add a field identifier alias
 *                                 (`AS`) if non-empty string (optional).
 * @return string   Returns the integer cast expression compatible with the
 *                  connection database platform.
 */
public function castInt(string $value, string $asIdentifier = ''): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('pages');
$queryBuilder
    ->select('uid')
    ->from('pages');

// simple value (quoted) to be used as sub-expression
$expression1 = $queryBuilder->expr()->castInt(
    $queryBuilder->quote('123'),
);

// simple value (quoted) to return as select field
$queryBuilder->addSelectLiteral(
    $queryBuilder->expr()->castInt(
        $queryBuilder->quote('123'),
        'virtual_field',
    ),
);

$expression3 = queryBuilder->expr()->castInt(
  $queryBuilder->quoteIdentifier('uid'),
);

// expression to be used as sub-expression
$expression4 = $queryBuilder->expr()->castInt(
    $queryBuilder->expr()->castVarchar('(1 * 10)'),
);

// expression to return as select field
$queryBuilder->addSelectLiteral(
    $queryBuilder->expr()->castInt(
        $queryBuilder->expr()->castVarchar('(1 * 10)'),
        'virtual_field',
    ),
);
Copied!

ExpressionBuilder::repeat() 

Create a statement to generate a value repeating defined $value for $numberOfRepeats times. This method can be used to provide the repeat number as a sub-expression or calculation.

Method signature
/**
 * @param int|string    $numberOfRepeats    Statement or value defining
 *                                          how often the $value should
 *                                          be repeated. Proper quoting
 *                                          must be ensured.
 * @param string        $value              Value which should be repeated.
 *                                          Proper quoting must be ensured.
 * @param string        $asIdentifier       Provide `AS` identifier if not
 *                                          empty.
 * @return string   Returns the platform compatible statement to create the
 *                  x-times repeated value.
 */
public function repeat(
    int|string $numberOfRepeats,
    string $value,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->repeat(
    10,
    $queryBuilder->quote('.'),
);

$expression2 = $queryBuilder->expr()->repeat(
    20,
    $queryBuilder->quote('0'),
    $queryBuilder->quoteIdentifier('aliased_field'),
);

$expression3 = $queryBuilder->expr()->repeat(
    20,
    $queryBuilder->quoteIdentifier('table_field'),
    $queryBuilder->quoteIdentifier('aliased_field'),
);

$expression4 = $queryBuilder->expr()->repeat(
    $queryBuilder->expr()->castInt(
        $queryBuilder->quoteIdentifier('repeat_count_field')
    ),
    $queryBuilder->quoteIdentifier('table_field'),
    $queryBuilder->quoteIdentifier('aliased_field'),
);

$expression5 = $queryBuilder->expr()->repeat(
    '(7 + 3)',
    $queryBuilder->quote('.'),
);

$expression6 = $queryBuilder->expr()->repeat(
  '(7 + 3)',
  $queryBuilder->concat(
    $queryBuilder->quote(''),
    $queryBuilder->quote('.'),
    $queryBuilder->quote(''),
  ),
  'virtual_field_name',
);
Copied!

ExpressionBuilder::space() 

Create statement containing $numberOfSpaces spaces.

Method signature
/**
 * @param int|string    $numberOfSpaces Expression or value defining how
 *                                      many spaces should be created.
 * @param string        $asIdentifier   Provide result as identifier field
 *                                      (AS), not added if empty string.
 * @return string   Returns the platform compatible statement to create the
 *                  x-times repeated space(s).
 */
public function space(
    string $numberOfSpaces,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->space(
    '10'
);

$expression2 = $queryBuilder->expr()->space(
    '20',
    $queryBuilder->quoteIdentifier('aliased_field'),
);

$expression3 = $queryBuilder->expr()->space(
    '(210)'
);

$expression3 = $queryBuilder->expr()->space(
    '(210)',
    $queryBuilder->quoteIdentifier('aliased_field'),
);

$expression5 = $queryBuilder->expr()->space(
    $queryBuilder->expr()->castInt(
        $queryBuilder->quoteIdentifier('table_repeat_number_field'),
    ),
);

$expression6 = $queryBuilder->expr()->space(
    $queryBuilder->expr()->castInt(
        $queryBuilder->quoteIdentifier('table_repeat_number_field'),
    ),
    $queryBuilder->quoteIdentifier('aliased_field'),
);
Copied!

ExpressionBuilder::left() 

Extract $length character of $value from the left side.

Method signature
/**
 * @param int|string    $length         Integer value or expression
 *                                      providing the length as integer.
 * @param string        $value          Value, identifier or expression
 *                                      defining the value to extract from
 *                                      the left.
 * @param string        $asIdentifier   Provide `AS` identifier if not empty.
 * @return string   Return the expression to extract defined substring
 *                  from the right side.
 */
public function left(
    int|string $length,
    string $value,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->left(
    6,
    $queryBuilder->quote('some-string'),
);

$expression2 = $queryBuilder->expr()->left(
    '6',
    $queryBuilder->quote('some-string'),
);

$expression3 = $queryBuilder->expr()->left(
    $queryBuilder->castInt('(23)'),
    $queryBuilder->quote('some-string'),
);

$expression4 = $queryBuilder->expr()->left(
    $queryBuilder->castInt('(23)'),
    $queryBuilder->quoteIdentifier('table_field_name'),
);
Copied!

ExpressionBuilder::right() 

Extract $length character of $value from the right side.

Method signature
/**
 * @param int|string    $length         Integer value or expression
 *                                      providing the length as integer.
 * @param string        $value          Value, identifier or expression
 *                                      defining the value to extract from
 *                                      the right.
 * @param string        $asIdentifier   Provide `AS` identifier if not empty.
 *
 * @return string   Return the expression to extract defined substring
 *                  from the right side.
 */
public function right(
    int|string $length,
    string $value,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->right(
    6,
    $queryBuilder->quote('some-string'),
);

$expression2 = $queryBuilder->expr()->right(
    '6',
    $queryBuilder->quote('some-string'),
);

$expression3 = $queryBuilder->expr()->right(
    $queryBuilder->castInt('(23)'),
    $queryBuilder->quote('some-string'),
);

$expression4 = $queryBuilder->expr()->right(
    $queryBuilder->castInt('(23)'),
    $queryBuilder->quoteIdentifier('table_field_name'),
);
Copied!

ExpressionBuilder::leftPad() 

Left-pad the value or sub-expression result with $paddingValue, to a total length of $length.

Method signature
/**
 * @param string        $value          Value, identifier or expression
 *                                      defining the value which should
 *                                      be left padded.
 * @param int|string    $length         Value, identifier or expression
 *                                      defining the padding length to
 *                                      fill up on the left or crop.
 * @param string        $paddingValue   Padding character used to fill
 *                                      up if characters are missing on
 *                                      the left side.
 * @param string        $asIdentifier   Used to add a field identifier alias
 *                                      (`AS`) if non-empty string (optional).
 * @return string   Returns database connection platform compatible
 *                  left-pad expression.
 */
public function leftPad(
    string $value,
    int|string $length,
    string $paddingValue,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->leftPad(
    $queryBuilder->quote('123'),
    10,
    '0',
);

$expression2 = $queryBuilder->expr()->leftPad(
    $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
    10,
    '0',
);

$expression3 = $queryBuilder->expr()->leftPad(
    $queryBuilder->expr()->concat(
        $queryBuilder->quote('1'),
        $queryBuilder->quote('2'),
        $queryBuilder->quote('3'),
    ),
    10,
    '0',
);

$expression4 = $queryBuilder->expr()->leftPad(
    $queryBuilder->castVarchar('( 1123 )'),
    10,
    '0',
);

$expression5 = $queryBuilder->expr()->leftPad(
    $queryBuilder->castVarchar('( 1123 )'),
    10,
    '0',
    'virtual_field',
);
Copied!

ExpressionBuilder::rightPad() 

Right-pad the value or sub-expression result with $paddingValue, to a total length of $length.

Method signature
/**
 * @param string        $value          Value, identifier or expression
 *                                      defining the value which should be
 *                                      right padded.
 * @param int|string    $length         Value, identifier or expression
 *                                      defining the padding length to
 *                                      fill up on the right or crop.
 * @param string        $paddingValue   Padding character used to fill up
 *                                      if characters are missing on the
 *                                      right side.
 * @param string        $asIdentifier   Used to add a field identifier alias
 *                                      (`AS`) if non-empty string (optional).
 * @return string   Returns database connection platform compatible
 *                  right-pad expression.
 */
public function rightPad(
    string $value,
    int|string $length,
    string $paddingValue,
    string $asIdentifier = '',
): string {}
Copied!
Usage example
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Utility\GeneralUtility;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('some_table');

$expression1 = $queryBuilder->expr()->rightPad(
    $queryBuilder->quote('123'),
    10,
    '0',
);

$expression2 = $queryBuilder->expr()->rightPad(
    $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
    10,
    '0',
 );

$expression3 = $queryBuilder->expr()->rightPad(
    $queryBuilder->expr()->concat(
        $queryBuilder->quote('1'),
        $queryBuilder->quote('2'),
        $queryBuilder->quote('3'),
    ),
    10,
    '0',
);

$expression4 = $queryBuilder->expr()->rightPad(
    $queryBuilder->castVarchar('( 1123 )'),
    10,
    '0',
);

$expression5 = $queryBuilder->expr()->rightPad(
    $queryBuilder->quote('123'),
    10,
    '0',
    'virtual_field',
);
Copied!

Impact 

Extension authors can use the new expression methods to build more advanced queries without the requirement to deal with the correct implementation for all supported database vendors.

Feature: #103331 - Native support for language Maltese added 

See forge#103331

Description 

TYPO3 now supports Maltese. Maltese language is spoken in Malta.

The ISO 639-1 code for Maltese is "mt", which is how TYPO3 accesses the language internally.

Impact 

It is now possible to

  • Fetch translated labels from translations.typo3.org / CrowdIn automatically within the TYPO3 backend.
  • Switch the backend interface to Maltese language.
  • Create a new language in a site configuration using Maltese.
  • Create translation files with the "mt" prefix (such as mt.locallang.xlf) to create your own labels.

TYPO3 will pick Maltese as a language just like any other supported language.

Feature: #103372 - Native support for language Irish Gaelic added 

See forge#103372

Description 

TYPO3 now supports Irish Gaelic. Irish Gaelic language is spoken in Ireland.

The ISO 639-1 code for Irish Gaelic is "ga", which is how TYPO3 accesses the language internally.

Impact 

It is now possible to

  • Fetch translated labels from translations.typo3.org / CrowdIn automatically within the TYPO3 backend.
  • Switch the backend interface to Irish Gaelic language.
  • Create a new language in a site configuration using Irish Gaelic.
  • Create translation files with the "ga" prefix (such as ga.locallang.xlf) to create your own labels.

TYPO3 will pick Irish Gaelic as a language just like any other supported language.

Feature: #103437 - Introduce site sets 

See forge#103437

Description 

Site sets ship parts of site configuration as composable pieces. They are intended to deliver settings, TypoScript, TSconfig and reference enabled content blocks for the scope of a site.

Extensions can provide multiple sets in order to ship presets for different sites or subsets (think of frameworks) where selected features are exposed as a subset (example: typo3/seo-xml-sitemap).

A set is defined in an extension's subfolder in Configuration/Sets/, for example EXT:my_extension/Configuration/Sets/MySet/config.yaml.

The folder name in Configuration/Sets/ is arbitrary, significant is the name defined in config.yaml. The name uses a vendor/name scheme by convention, and should use the same vendor as the containing extension. It may differ if needed for compatibility reasons (e.g. when sets are moved to other extensions). If an extension provides exactly one set that should have the same name as defined in composer.json.

The config.yaml for a set that is composed of three subsets looks as follows:

EXT:my_extension/Configuration/Sets/MySet/config.yaml
name: my-vendor/my-set
label: My Set

# Load TypoScript, TSconfig and settings from dependencies
dependencies:
  - some-namespace/slider
  - other-namespace/fancy-carousel
Copied!

Sets are applied to sites via dependencies array in site configuration:

config/sites/my-site/config.yaml
base: 'http://example.com/'
rootPageId: 1
dependencies:
  - my-vendor/my-set
Copied!

Site sets can also be edited via the backend module Site Management > Sites.

A list of available site sets can be retrieved with the console command bin/typo3 site:sets:list.

Settings definitions 

Sets can define settings definitions which contain more metadata than just a value: They contain UI-relevant options like label, description, category and tags and types like int, bool, string, stringlist, text or color. These definitions are placed in settings.definitions.yaml next to the site set file config.yaml.

The description can make use of markdown syntax for richtext formatting.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  foo.bar.baz:
    label: 'My example baz setting'
    description: 'Configure `baz` to be used in `bar`.'
    type: int
    default: 5
Copied!

Settings for subsets 

Settings for subsets (e.g. to configure settings in declared dependencies) can be shipped via settings.yaml when placed next to the set file config.yaml.

Note that default values for settings provided by the set do not need to be defined here, as defaults are to be provided within settings.definitions.yaml.

Here is an example where the setting styles.content.defaultHeaderType — as provided by typo3/fluid-styled-content — is configured via settings.yaml:

EXT:my_extension/Configuration/Sets/MySet/settings.yaml
styles.content.defaultHeaderType: 1
Copied!

This setting will be exposed as site setting whenever the set my-vendor/my-set is applied to a site configuration.

Hidden sets 

Sets may be hidden from the backend set selection in Site Management > Sites and the console command bin/typo3 site:sets:list by adding a hidden flag to the config.yaml definition:

EXT:my_extension/Configuration/Sets/MyHelperSet/config.yaml
name: my-vendor/my-helperset
label: A helper Set that is not visible inside the GUI
hidden: true
Copied!

Integrators may choose to hide existing sets from the list of available sets for backend users via User TSConfig, in case only a curated list of sets shall be selectable:

EXT:my_extension/Configuration/user.tsconfig
options.sites.hideSets := addToList(typo3/fluid-styled-content)
Copied!

The Site Management > Sites GUI will not show hidden sets, but makes one exception if a hidden set has already been applied to a site (e.g. by manual modification of config.yaml). In this case a set marked as hidden will be shown in the list of currently activated sets (that means it can be introspected and removed via backend UI).

Impact 

Sites can be composed of sets where relevant configuration, templates, assets and setting definitions are combined in a central place and applied to sites as one logical volume.

Sets have dependency management and therefore allow sharing code between multiple TYPO3 sites and extensions in a flexible way.

Feature: #103439 - TypoScript provider for sites and sets 

See forge#103439

Description 

TYPO3 sites have been enhanced to be able to operate as TypoScript template provider. They act similar to sys_template records with "clear" and "root" flags set. By design a site TypoScript provider always defines a new scope ("root" flag) and does not inherit from parent sites (for example, sites up in the root line). That means it behaves as if the "clear" flag is set in a sys_template record. This behavior is not configurable by design, as TypoScript code sharing is intended to be implemented via sharable sets (Feature: #103437 - Introduce site sets).

Note that sys_template records will still be loaded, but they are optional now, and applied after TypoScript provided by the site.

TypoScript dependencies can be included via set dependencies. This mechanism is much more effective than the previous static_file_include's or manual @import statements (they are still fine for local includes, but should be avoided for cross-set/extensions dependencies), as sets are automatically ordered and deduplicated.

Site TypoScript 

The files setup.typoscript and constants.typoscript (placed next to the site's config.yaml file) will be loaded as TypoScript setup and constants, if available.

Site dependencies (sets) will be loaded first, that means setup and constants can be overridden on a per-site basis.

Set TypoScript 

Set-defined TypoScript can be shipped within a set. The files setup.typoscript and constants.typoscript (placed next to the config.yaml file) will be loaded, if available. They are inserted (similar to static_file_include) into the TypoScript chain of the site TypoScript that will be defined by a site that is using sets.

Set constants will always be overruled by site settings. Since site settings always provide a default value, a constant will always be overruled by a defined setting. This can be used to provide backward compatibility with TYPO3 v12 in extensions, where constants shall be used in v12, while v13 will always prefer defined site settings.

In contrast to static_file_include, dependencies are to be included via sets. Dependencies are included recursively. This mechanism supersedes the previous include via static_file_include or manual @import statements as sets are automatically ordered and deduplicated. That means TypoScript will not be loaded multiple times, if a shared dependency is required by multiple sets.

Note that @import statements are still fine to be used for local includes, but should be avoided for cross-set/extensions dependencies.

Global TypoScript 

Site sets introduce reliable dependencies in order to replace the need for globally provided TypoScript. It is therefore generally discouraged to use global TypoScript in an environment using TypoScript provided by site sets. TypoScript should only be provided globally if absolutely needed.

It has therefore been decided that ext_typoscript_setup.typoscript and ext_typoscript_constants.typoscript are not autoloaded in site set provided TypoScript.

These files can still be used to provide global TypoScript for traditional sys_template setups. Existing setups do not need to be adapted and extensions can still ship globally defined TypoScript via ext_typoscript_setup.typoscript for these cases, but should provide explicitly dependable sets for newer site set setups.

If global TypoScript is still needed and is unavoidable, it can be provided for site sets and sys_template setups in ext_localconf.php via:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
    'module.tx_foo.settings.example = 1'
);
Copied!

There are some cases where globally defined TypoScript configurations are needed because backend modules rely on their availability. One such case is the form framework backend module which uses module.tx_form.settings.yamlConfigurations as a registry for extension-provided form configuration. Global form configuration can be loaded via ExtensionManagementUtility as described in YAML registration for the backend via addTypoScriptSetup() Please make sure to only load backend-related form TypoScript globally and to provide TypoScript related to frontend rendering via site sets.

Impact 

Sites and sets can ship TypoScript without the need for sys_template records in database, and dependencies can be expressed via sets, allowing for automatic ordering and deduplication.

Feature: #103441 - Request ID as public visible error reference in error handlers output 

See forge#103441

Description 

The ProductionExceptionHandler in EXT:core outputs error details, but not for everyone. As a normal visitor you don't see any traceable error information.

The ProductionExceptionHandler in EXT:frontend outputs "Oops, an error occurred!" followed by a timestamp and a hash. This is part of log messages.

Whenever an error/exception is logged, the log message contains the request ID.

With this the request ID is also shown in web output of error/exception handlers as public visible error reference.

Impact 

Everyone sees a request id as traceable error information.

Feature: #103504 - New ContentObject PAGEVIEW 

See forge#103504

Description 

A new content object for TypoScript PAGEVIEW has been added.

This cObject is mainly intended for rendering a full page in the TYPO3 frontend with fewer configuration options over the generic FLUIDTEMPLATE cObject.

A basic usage of the PAGEVIEW cObject is as follows:

page = PAGE
page.10 = PAGEVIEW
page.10.paths.100 = EXT:mysite/Resources/Private/Templates/
Copied!

PAGEVIEW wires certain parts automatically:

  1. The name of the used page layout (backend layout) is resolved automatically.

    If a page has a layout named "with_sidebar", the template file is then resolved to EXT:mysite/Resources/Private/Templates/Pages/With_sidebar.html.

  2. Fluid features for layouts and partials are wired automatically. They can be placed into EXT:mysite/Resources/Private/Templates/Layouts/ and EXT:mysite/Resources/Private/Templates/Partials/ with above example.
  3. Default variables are available in the Fluid template:

    • settings - contains all TypoScript settings (= constants)
    • site - the current Site object
    • language - the current SiteLanguage object
    • page - the current page record as object

There is no special Extbase resolving done for the templates.

Migration 

Before 

page = PAGE
page {
    10 = FLUIDTEMPLATE
    10 {
        templateName = TEXT
        templateName {
            stdWrap {
                cObject = TEXT
                cObject {
                    data = levelfield:-2, backend_layout_next_level, slide
                    override {
                        field = backend_layout
                    }
                    split {
                        token = pagets__
                        1 {
                            current = 1
                            wrap = |
                        }
                    }
                }
                ifEmpty = Standard
            }
        }

        templateRootPaths {
            100 = {$plugin.tx_mysite.templateRootPaths}
        }

        partialRootPaths {
            100 = {$plugin.tx_mysite.partialRootPaths}
        }

        layoutRootPaths {
            100 = {$plugin.tx_mysite.layoutRootPaths}
        }

        variables {
            pageUid = TEXT
            pageUid.data = page:uid

            pageTitle = TEXT
            pageTitle.data = page:title

            pageSubtitle = TEXT
            pageSubtitle.data = page:subtitle

            parentPageTitle = TEXT
            parentPageTitle.data = levelfield:-1:title
        }

        dataProcessing {
            10 = menu
            10.as = mainMenu
        }
    }
}
Copied!

After 

page = PAGE
page {
    10 = PAGEVIEW
    10 {
        paths {
            100 = {$plugin.tx_mysite.templatePaths}
        }
        variables {
            parentPageTitle = TEXT
            parentPageTitle.data = levelfield:-1:title
        }
        dataProcessing {
            10 = menu
            10.as = mainMenu
        }
    }
}
Copied!

In Fluid, the pageUid is available as {page.uid} and pageTitle as {page.title}. The page layout identifier can be accessed using {page.pageLayout.identifier}.

Impact 

Creating new page templates based on Fluid follows conventions in order to reduce the amount of TypoScript needed to render a page in the TYPO3 frontend.

Sane defaults are applied, variables and settings are available at any time.

Feature: #103522 - Page TSconfig provider for sites and sets 

See forge#103522

Description 

TYPO3 sites have been enhanced to be able to provide page TSconfig on a per-site basis.

Site page TSconfig is loaded from page.tsconfig, if placed next to the site configuration file config.yaml and is scoped to pages within that site.

Impact 

Sites and sets can ship page TSconfig without the need for database entries or by polluting global scope when registering page TSconfig globally via ext_localconf.php or Configuration/page.tsconfig. Dependencies can be expressed via sets, allowing for automatic ordering and deduplication.

Feature: #103529 - Introduce hotkey for "Save and Close" 

See forge#103529

Description 

A new hotkey is introduced in the FormEngine scope that lets editors invoke "Save and Close" via Ctrl/Cmd + Shift + S.

Impact 

Next to the existing Ctrl/Cmd + s hotkey (Save), the hotkey Ctrl/Cmd + Shift + S (Save and Close) became available.

Feature: #103560 - Update Fluid Standalone to version 2.11 

See forge#103560

Description 

Fluid Standalone has been updated to version 2.11. This version includes new ViewHelpers that cover common tasks in Fluid templates. More ViewHelpers will be added with future minor releases.

A full documentation of the new ViewHelper's arguments is available in the ViewHelper reference <https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/>.

Impact 

The following ViewHelpers are now included and can be used in all Fluid templates:

<f:split> ViewHelper: 

The SplitViewHelper splits a string by the specified separator, which results in an array.

<f:split value="1,5,8" separator="," /> <!-- Output: {0: '1', 1: '5', 2: '8'} -->
<f:split separator="-">1-5-8</f:split> <!-- Output: {0: '1', 1: '5', 2: '8'} -->
<f:split value="1,5,8" separator="," limit="2" /> <!-- Output: {0: '1', 1: '5,8'} -->
Copied!

<f:join> ViewHelper: 

The JoinViewHelper combines elements from an array into a single string.

<f:join value="{0: '1', 1: '2', 2: '3'}" /> <!-- Output: 123 -->
<f:join value="{0: '1', 1: '2', 2: '3'}" separator=", " /> <!-- Output: 1, 2, 3 -->
<f:join value="{0: '1', 1: '2', 2: '3'}" separator=", " separatorLast=" and " /> <!-- Output: 1, 2 and 3 -->
Copied!

<f:replace> ViewHelper: 

The ReplaceViewHelper replaces one or multiple strings with other strings.

<f:replace value="Hello World" search="World" replace="Fluid" /> <!-- Output: Hello Fluid -->
<f:replace value="Hello World" search="{0: 'World', 1: 'Hello'}" replace="{0: 'Fluid', 1: 'Hi'}" /> <!-- Output: Hi Fluid -->
<f:replace value="Hello World" replace="{'World': 'Fluid', 'Hello': 'Hi'}" /> <!-- Output: Hi Fluid -->
Copied!

<f:first> and <f:last> ViewHelpers: 

The FirstViewHelper and LastViewHelper return the first or last item of a specified array, respectively.

<f:first value="{0: 'first', 1: 'second', 2: 'third'}" /> <!-- Outputs "first" -->
<f:last value="{0: 'first', 1: 'second', 2: 'third'}" /> <!-- Outputs "third" -->
Copied!

Feature: #103578 - Add database default value support for TEXT, BLOB and JSON field types 

See forge#103578

Description 

Database default values for TEXT, JSON and BLOB fields could not be used in a cross-database, vendor-compatible manner, for example in ext_tables.sql, or as default database scheme generation for TCA-managed tables and types.

Direct default values are still unsupported, but since MySQL 8.0.13+ this is possible by using default value expressions, albeit in a slightly differing syntax.

Example 

EXT:my_extension/ext_tables.sql
CREATE TABLE `tx_myextension_domain_model_entity` (
  `some_field` TEXT NOT NULL DEFAULT 'default-text',
  `json_field` JSON NOT NULL DEFAULT '{}'
);
Copied!
Insert a new record using the defined default values
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionByName(ConnectionPool::DEFAULT_NAME);
$connection->insert(
    'tx_myextension_domain_model_entity',
    [
        'pid' => 123,
    ]
);
Copied!

Advanced example with value quoting 

EXT:my_extension/ext_tables.sql
CREATE TABLE a_textfield_test_table
(
    # JSON object default value containing single quote in json field
    field1 JSON NOT NULL DEFAULT '{"key1": "value1", "key2": 123, "key3": "value with a '' single quote"}',

    # JSON object default value containing double-quote in json field
    field2 JSON NOT NULL DEFAULT '{"key1": "value1", "key2": 123, "key3": "value with a \" double quote"}',
);
Copied!

Impact 

Database INSERT queries that do not provide values for fields with defined default values, and that do not use TCA-powered TYPO3 APIs, can now be used, and will receive default values defined at databaselevel. This also accounts for dedicated applications operating directly on the database table.

Feature: #103671 - Provide null coalescing operator for TypoScript constants 

See forge#103671

Description 

TypoScript constants expressions have been extended to support a null coalescing operator (??) as a way for providing a migration path from a legacy constant name to a newer name, while providing full backwards compatibility for the legacy constant name, if still defined.

Example that evaluates to $config.oldThing if set, otherwise the newer setting $myext.thing would be used:

plugin.tx_myext.settings.example = {$config.oldThing ?? $myext.thing}
Copied!

Impact 

Since Feature: #103439 - TypoScript provider for sites and sets it is suggested to define site settings via settings.definitions.yaml in site sets instead of TypoScript constants. Migration of TYPO3 Core extensions revealed that such migration is a good time to revisit constant names and the null coalescing operator helps to switch to a new setting identifier without breaking backwards-compatibility with previous constant names.

Deprecation: #102762 - Deprecate GeneralUtility::hmac() 

See forge#102762

Description 

The method \TYPO3\CMS\Core\Utility\GeneralUtility::hmac() has been deprecated in TYPO3 v13 and will be removed with v14 in favor of Feature: #102761 - Introduce class to generate/validate HMAC hashes.

Impact 

Usage of the method will raise a deprecation level log entry in TYPO3 v13 and a fatal error in TYPO3 v14.

Affected installations 

All third-party extensions using \TYPO3\CMS\Core\Utility\GeneralUtility::hmac().

Migration 

All usages of \TYPO3\CMS\Core\Utility\GeneralUtility::hmac() must be migrated to use the hmac() method in the class \TYPO3\CMS\Core\Crypto\HashService .

Before 

//use TYPO3\CMS\Core\Utility\GeneralUtility;

$hmac = GeneralUtility::hmac('some-input', 'some-secret');
Copied!

After 

Using GeneralUtility::makeInstance()
//use TYPO3\CMS\Core\Crypto\HashService;
//use TYPO3\CMS\Core\Utility\GeneralUtility;

$hashService = GeneralUtility::makeInstance(HashService::class);
$hmac = $hashService->hmac('some-input', 'some-secret');
Copied!
Using dependency injection
namespace MyVendor\MyExt\Services;

use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final readonly class MyService
{
    public function __construct(
        private HashService $hashService,
    ) {}

    public function someMethod(): void
    {
        $hmac = $this->hashService->hmac('some-input', 'some-secret');
    }
}
Copied!

If possible, use dependency injection to inject HashService into your class.

Deprecation: #103211 - Deprecate pageTree.backgroundColor 

See forge#103211

Description 

The user TSconfig option options.pageTree.backgroundColor has been deprecated and will be removed in TYPO3 v14 due to its lack of accessibility. It is being replaced with a new label system for tree nodes.

Impact 

During v13, options.pageTree.backgroundColor will be migrated to the new label system. Since the use case is unknown, the generated label will be "Color: <value>". This information will be displayed on all affected nodes.

Affected installations 

All installations that use the user TSconfig option options.pageTree.backgroundColor are affected.

Migration 

Before:

EXT:my_extension/Configuration/user.tsconfig
options.pageTree.backgroundColor.<pageid> = #ff8700
Copied!

After:

EXT:my_extension/Configuration/user.tsconfig
options.pageTree.label.<pageid> {
    label = Campaign A
    color = #ff8700
}
Copied!

Deprecation: #103230 - Deprecate @typo3/backend/wizard.js 

See forge#103230

Description 

The TYPO3 backend module @typo3/backend/wizard.js that offers simple wizards has been marked as deprecated in favor of the richer @typo3/backend/multi-step-wizard.js module.

Impact 

Using the deprecated module will trigger a browser console warning.

Affected installations 

All installations using @typo3/backend/wizard.js are affected.

Migration 

Migrate to the module @typo3/backend/multi-step-wizard.js. There are two major differences:

  • The class name changes to MultiStepWizard.
  • The method addSlide() receives an additional argument for the step title in the progress bar.

Example 

-import Wizard from '@typo3/backend/wizard.js';
+import MultiStepWizard from '@typo3/backend/multi-step-wizard.js';

-Wizard.addSlide(
+MultiStepWizard.addSlide(
     'my-slide-identifier',
     'Slide title',
     'Content of my slide',
     SeverityEnum.notice,
+    'My step',
     function () {
         // callback executed after displaying the slide
     }
);
Copied!

Deprecation: #103244 - Class SlugEnricher 

See forge#103244

Description 

Class \TYPO3\CMS\Core\DataHandling\SlugEnricher has been marked as deprecated in TYPO3 v13 and will be removed with v14.

The class was used as a helper for \TYPO3\CMS\Core\DataHandling\DataHandler , which now inlines the code in a simplified variant.

Impact 

Using the class will raise a deprecation level log entry and a fatal error in TYPO3 v14.

Affected installations 

There is little to no reason to use this class in custom extensions, very few instances should be affected by this. The extension scanner will find usages with a strong match.

Migration 

No migration available.

Deprecation: #103528 - Deprecated DocumentSaveActions module 

See forge#103528

Description 

The JavaScript module @typo3/backend/document-save-actions.js was introduced in TYPO3 v7 to add some interactivity in FormEngine context. At first it was only used to disable the submit button and render a spinner icon instead. Over the course of some years, the module got more functionality, for example to prevent saving when validation fails.

Since some refactorings within FormEngine, the module rather became a burden. This became visible with the introduction of the Hotkeys API, as the @typo3/backend/document-save-actions.js reacts on explicit click events on the save icon, that is not triggered when FormEngine invokes a save action via keyboard shortcuts. Adjusting document-save-actions.js's behavior is necessary, but would become a breaking change, which is unacceptable after the 13.0 release. For this reason, said module has been marked as deprecated and its usages are replaced by its successor @typo3/backend/form/submit-interceptor.js.

Impact 

Using the JavaScript module @typo3/backend/document-save-actions.js will render a deprecation warning in the browser's console.

Affected installations 

All installations relying on @typo3/backend/document-save-actions.js are affected.

Migration 

To migrate the interception of submit events, the successor module @typo3/backend/form/submit-interceptor.js shall be used instead.

The usage is similar to @typo3/backend/document-save-actions.js, but requires the form HTML element in its constructor.

Example 

import '@typo3/backend/form/submit-interceptor.js';

// ...

const formElement = document.querySelector('form');
const submitInterceptor = new SubmitInterceptor(formElement);
submitInterceptor.addPreSubmitCallback(function() {
    // the same handling as in @typo3/backend/document-save-actions.js
});
Copied!

Deprecation: #103850 - Renamed Page Tree Navigation Component ID 

See forge#103850

Description 

When registering a module in the TYPO3 Backend, using the page tree as navigation component, the name of the page tree navigation component has been renamed in TYPO3 v13.

Previously, the navigation component was called @typo3/backend/page-tree/page-tree-element, now it is named @typo3/backend/tree/page-tree-element.

Impact 

Using the old navigation ID will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations with custom backend modules utilizing the page tree navigation component.

Migration 

Instead of writing this snippet in your Configuration/Backend/Modules.php:

'mymodule' => [
    'parent' => 'web',
    ...
    'navigationComponent' => '@typo3/backend/page-tree/page-tree-element',
],
Copied!

It is now called:

'mymodule' => [
    'parent' => 'web',
    ...
    'navigationComponent' => '@typo3/backend/tree/page-tree-element',
],
Copied!

Important: #103165 - Database table cache_treelist removed 

See forge#103165

Description 

Database table cache_treelist has been removed, the database analyzer will suggest to drop it if it exists.

That cache table was unused since a TYPO3 v12 patch level release, v13 removed leftover handling throughout the Core and removed the table itself.

13.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: #97330 - FormEngine element classes must create label or legend 

See forge#97330

Description 

When editing records in the backend, the FormEngine class structure located within EXT:backend/Classes/Form/ handles the generation of the editing view.

A change has been applied related to the rendering of single field labels, which is no longer done automatically by "container" classes: Single elements have to create the label themselves.

Extension that add own elements to FormEngine must be adapted, otherwise the element label is no longer rendered.

Impact 

When the required changes are not applied to custom FormEngine element classes, the value of the TCA "label" property is not rendered.

Affected installations 

Instances with custom FormEngine elements are affected. Custom elements need to be registered to the FormEngine's NodeFactory, candidates are found by looking at the $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine'] array (for instance using the System > Configuration backend module provided by EXT:lowlevel). Classes registered using the sub keys nodeRegistry and nodeResolver may be affected. The extension scanner does not find affected classes.

Migration 

Custom elements must take care of creating a <label> or <legend> tag on their own: If the element creates an <input>, or <select> tag, the <label> should have a for attribute that points to a field having an id attribute. This is important especially for accessibility. When no such target element exists, a <legend> embedded in a <fieldset> can be used. There are two helper methods in \TYPO3\CMS\Backend\Form\Element\AbstractFormElement to help with this: renderLabel() and wrapWithFieldsetAndLegend().

In practice, an element having an <input>, or <select> field should essentially look like this:

$resultArray = $this->initializeResultArray();
// Next line is only needed for extensions that need to keep TYPO3 v12 compatibility
$resultArray['labelHasBeenHandled'] = true;
$fieldId = StringUtility::getUniqueId('formengine-input-');
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] =     '<div class="form-wizards-wrap">';
$html[] =         '<div class="form-wizards-element">';
$html[] =             '<div class="form-control-wrap">';
$html[] =                 '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" value="..." type="text">';
$html[] =             '</div>';
$html[] =         '</div>';
$html[] =     '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
Copied!

The renderLabel() is a helper method to generate a <label> tag with a for attribute, and the same fieldId is used as id attribute in the <input> field to connect <label> and <input> with each other.

If there is no such field, a <legend> tag should be used:

$resultArray = $this->initializeResultArray();
// Next line is only needed for extensions that need to keep TYPO3 v12 compatibility
$resultArray['labelHasBeenHandled'] = true;
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] =     '<div class="form-wizards-wrap">';
$html[] =         '<div class="form-wizards-element">';
$html[] =             '<div class="form-control-wrap">';
$html[] =                 Some custom element html
$html[] =             '</div>';
$html[] =         '</div>';
$html[] =     '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
Copied!

Breaking: #97664 - FormPersistenceManagerInterface modified 

See forge#97664

Description 

The PHP interface \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface now requires PHP classes to implement an additional method hasForms() in order to fulfill the API.

Impact 

TYPO3 projects with extensions using implementations of the \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface will now break with a fatal PHP error.

Affected installations 

Extensions using implementations of the \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface .

Migration 

Add the new method to your extension's implementation of this interface, which also makes it compatible with TYPO3 v12 and TYPO3 v13 at the same time.

Breaking: #99323 - Removed hook for modifying records after fetching content 

See forge#99323

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] has been removed in favor of the more powerful PSR-14 \TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent .

Impact 

Any hook implementation registered is not executed anymore in TYPO3 v13.0+.

Affected installations 

TYPO3 installations with custom extensions using this hook.

Migration 

The hook is removed without deprecation in order to allow extensions to work with TYPO3 v12 (using the hook) and v13+ (using the new event) when implementing the event as well without any further deprecations. Use the PSR-14 event to allow greater influence in the functionality.

Breaking: #99807 - Relocated ModifyUrlForCanonicalTagEvent 

See forge#99807

Description 

The \TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent has been improved.

Therefore, the event is now always dispatched after the standard functionality has been executed, such as fetching the URL from the page properties.

The event is furthermore also dispatched in case the canonical tag generation has been disabled via TypoScript or the page properties. This allows greater influence in the generation process, but might break existing setups, which rely on listeners are being called before standard functionality has been executed or only in case generation is enabled.

Effectively, this also means that getUrl() might already return a non-empty string.

Impact 

The \TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent is now always dispatched when generating canonical tags, just before the final tag markup is being built.

Affected installations 

TYPO3 installations with custom extensions, whose event listeners rely on the event being dispatched before standard functionality has been executed or only in case generation has not been disabled.

Migration 

Adjust your listeners by respecting the new execution order. Therefore, the event contains the new getCanonicalGenerationDisabledException() method, which can be used to determine whether generation is disabled and the reason for it.

Breaking: #99898 - Continuous array keys from GeneralUtility::intExplode 

See forge#99898

Description 

When the method \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode() is called with the parameter $removeEmptyEntries set to true, the array keys are now continuous.

Previously, the array had gaps in the keys in the places where empty values were removed. This behavior had been an undocumented side-effect of the implementation. It is now changed to always return an array with continuous integer array keys (i.e., a list) to reduce surprising behavior.

Before this change (TYPO3 v12):

GeneralUtility::intExplode(',', '1,,3', true);
// Result: [0 => 1, 2 => 3]
Copied!

After this change (TYPO3 v13):

GeneralUtility::intExplode(',', '1,,3', true);
// Result: [0 => 1, 1 => 3]
Copied!

Impact 

Calling GeneralUtility::intExplode() with the parameter $removeEmptyEntries set to true and relying on gaps in the keys of the resulting array keys may lead to unexpected results.

Affected installations 

Custom extensions that rely on the array keys of the result of GeneralUtility::intExplode() to have gaps in the keys.

Migration 

Adapt your code to not rely on gaps in the keys anymore.

Breaking: #99937 - Utilize BIGINT database column type for datetime TCA 

See forge#99937

Description 

The TCA 'type' => 'datetime' attribute previously created integer signed types as per the new auto-creation of table columns, if not specified differently via ext_tables.sql or listed as an exception ( starttime, endtime, tstamp, crdate).

A datetime field created without an exception would allow date ranges from 1901 to 2038. While that allows dates before 1970 (usual birthdays), sadly this field would "end" in 2038.

Because of this, the exceptions ( starttime, endtime, tstamp, crdate) already are created as integer unsigned, which puts them from 1970 to 2106. Dates before 1970 are not needed, because you will not publish or create anything in the past, but maybe after 2038.

However, there are many use cases where datetime TCA fields should have a much broader time span, or at least past 2038.

Now, all these fields are changed to use the bigint signed data type. This allows to define ranges far into the future and past. It uses a few more bytes within the database, for the benefit of being a unified solution that can apply to every use case.

Impact 

All extensions that previously declared datetime columns should remove the column definition from ext_tables.sql to utilize the type bigint signed. This will allow to store timestamps after 2038 (and before 1970).

A future implementation may change from integer-based columns completely to a native datetime database field.

When executing the database compare utility, the column definitions for a few Core fields are changed and their storable range increases.

These fields are now able to hold a timestamp beyond 2038 (and also before 1970):

  • be_users.lastlogin
  • fe_users.lastlogin
  • pages.lastUpdated
  • pages.newUntil
  • sys_redirect.lasthiton
  • index_config.timer_next_indexing
  • tx_extensionmanager_domain_model_extension.last_updated
  • sys_workspace.publish_time
  • sys_file_metadata.content_creation_date
  • tt_content.date

Breaking: #100224 - MfaViewType migrated to backed enum 

See forge#100224

Description 

The class \TYPO3\CMS\Core\Authentication\Mfa\MfaViewType has been migrated to a native PHP backed enum.

Impact 

Since MfaViewType is no longer a class, the existing class constants are no longer available, but are enum instances instead.

In addition, it's not possible to instantiate the class anymore or call the equals() method.

The \TYPO3\CMS\Core\Authentication\Mfa\MfaProviderInterface , which all MFA providers need to implement, does now require the third argument $type of the handleRequest() method to be a MfaViewType instead of a string.

Affected installations 

All installations directly using the class constants, instantiating the class or calling the equals() method.

All extensions with custom MFA providers, which therefore implement the handleRequest() method.

Migration 

To access the string representation of a MfaViewType, use the corresponding value property, e.g. \TYPO3\CMS\Core\Authentication\Mfa\MfaViewType::SETUP->value or on a variable, use $type->value.

Replace class instantiation by \TYPO3\CMS\Core\Authentication\Mfa\MfaViewType::tryFrom('setup').

Adjust your MFA providers handleRequest() method to match the interface:

public function handleRequest(
    ServerRequestInterface $request,
    MfaProviderPropertyManager $propertyManager,
    MfaViewType $type
): ResponseInterface;
Copied!

Breaking: #100229 - Convert JSConfirmation to a BitSet 

See forge#100229

Description 

The class \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation is replaced by \TYPO3\CMS\Core\Authentication\JsConfirmation . The new class is extending the \TYPO3\CMS\Core\Type\BitSet class instead of \TYPO3\CMS\Core\TypeEnumeration\Enumeration.

Impact 

Since JSConfirmation is now extending the class \TYPO3\CMS\Core\Type\BitSet it's no longer possible to call the following public methods:

  • matches()
  • setValue()
  • isValid()

The only static method left is: compare()

Affected installations 

Custom TYPO3 extensions calling public methods:

  • matches()
  • setValue()
  • isValid()

Custom TYPO3 extensions calling static methods in \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation except for the method \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation::compare().

Custom TYPO3 extensions calling \TYPO3\CMS\Core\Authentication\BackendUserAuthentication->jsConfirmation(), if first argument passed is not an int.

Migration 

Replace existing usages of \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation with \TYPO3\CMS\Core\Authentication\JsConfirmation .

There is no migration for the methods:

  • matches()
  • setValue()
  • isValid()

Remove existing calls to static methods \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation::method() and where JSConfirmation::compare() is used, replace the namespace from \TYPO3\CMS\Core\Type\Bitmask\JSConfirmation to \TYPO3\CMS\Core\Authentication\JsConfirmation .

Ensure an int value is passed to:

  • \TYPO3\CMS\Core\Authentication\BackendUserAuthentication->jsConfirmation()

Breaking: #100963 - Deprecated functionality removed 

See forge#100963

Description 

The following PHP classes that have previously been marked as deprecated with v12 have been removed:

  • \TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher
  • \TYPO3\CMS\Backend\EventListener\SilentSiteLanguageFlagMigration
  • \TYPO3\CMS\Backend\Template\Components\Buttons\Action\HelpButton
  • \TYPO3\CMS\Backend\Tree\View\BrowseTreeView
  • \TYPO3\CMS\Backend\Tree\View\ElementBrowserPageTreeView
  • \TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader
  • \TYPO3\CMS\Core\Configuration\PageTsConfig
  • \TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser
  • \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher
  • \TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException
  • \TYPO3\CMS\Core\Controller\RequireJsController
  • \TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction
  • \TYPO3\CMS\Core\Exception\MissingTsfeException
  • \TYPO3\CMS\Core\ExpressionLanguage\DeprecatingRequestWrapper
  • \TYPO3\CMS\Core\Resource\Service\MagicImageService
  • \TYPO3\CMS\Core\Resource\Service\UserFileInlineLabelService
  • \TYPO3\CMS\Core\Resource\Service\UserFileMountService
  • \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
  • \TYPO3\CMS\Core\TypoScript\TemplateService
  • \TYPO3\CMS\Core\Utility\ResourceUtility
  • \TYPO3\CMS\Dashboard\Views\Factory
  • \TYPO3\CMS\Fluid\ViewHelpers\Be\Buttons\CshViewHelper
  • \TYPO3\CMS\Fluid\ViewHelpers\Be\Labels\CshViewHelper
  • \TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher
  • \TYPO3\CMS\Frontend\Plugin\AbstractPlugin

The following PHP classes have been declared final:

  • \TYPO3\CMS\Core\Database\Driver\PDOMySql\Driver
  • \TYPO3\CMS\Core\Database\Driver\PDOPgSql\Driver
  • \TYPO3\CMS\Core\Database\Driver\PDOSqlite\Driver

The following PHP interfaces that have previously been marked as deprecated with v12 have been removed:

  • \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
  • \TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface
  • \TYPO3\CMS\Backend\Wizard\NewContentElementWizardHookInterface
  • \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface
  • \TYPO3\CMS\Core\Domain\Repository\PageRepositoryGetPageOverlayHookInterface
  • \TYPO3\CMS\Core\Domain\Repository\PageRepositoryGetRecordOverlayHookInterface
  • \TYPO3\CMS\Dashboard\Widgets\RequireJsModuleInterface
  • \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuFilterPagesHookInterface
  • \TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface
  • \TYPO3\CMS\Frontend\Http\UrlProcessorInterface

The following PHP interfaces changed:

  • \TYPO3\CMS\Adminpanel\ModuleApi\ShortInfoProviderInterface method setModuleData() added
  • \TYPO3\CMS\Backend\Form\NodeInterface method setData() added
  • \TYPO3\CMS\Backend\Form\NodeInterface method render() must return array
  • \TYPO3\CMS\Backend\Form\NodeResolverInterface method setData() added
  • \TYPO3\CMS\Backend\Form\NodeResolverInterface method resolve() must return ?string
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface method getContentObject() removed
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface method isFeatureEnabled() removed
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface method setContentObject() removed
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface method setRequest() added

The following PHP class aliases that have previously been marked as deprecated with v12 have been removed:

  • \TYPO3\CMS\Backend\ElementBrowser\FileBrowser
  • \TYPO3\CMS\Backend\ElementBrowser\FolderBrowser
  • \TYPO3\CMS\Backend\Form\Element\InputColorPickerElement
  • \TYPO3\CMS\Backend\Form\Element\InputDateTimeElement
  • \TYPO3\CMS\Backend\Form\Element\InputLinkElement
  • \TYPO3\CMS\Backend\Provider\PageTsBackendLayoutDataProvider
  • \TYPO3\CMS\Frontend\Service\TypoLinkCodecService
  • \TYPO3\CMS\Frontend\Typolink\LinkResultFactory
  • \TYPO3\CMS\Recordlist\Browser\AbstractElementBrowser
  • \TYPO3\CMS\Recordlist\Browser\DatabaseBrowser
  • \TYPO3\CMS\Recordlist\Browser\ElementBrowserInterface
  • \TYPO3\CMS\Recordlist\Browser\ElementBrowserRegistry
  • \TYPO3\CMS\Recordlist\Browser\FileBrowser
  • \TYPO3\CMS\Recordlist\Browser\FolderBrowser
  • \TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController
  • \TYPO3\CMS\Recordlist\Controller\AccessDeniedException
  • \TYPO3\CMS\Recordlist\Controller\ClearPageCacheController
  • \TYPO3\CMS\Recordlist\Controller\ElementBrowserController
  • \TYPO3\CMS\Recordlist\Controller\RecordDownloadController
  • \TYPO3\CMS\Recordlist\Controller\RecordListController
  • \TYPO3\CMS\Recordlist\Event\ModifyRecordListHeaderColumnsEvent
  • \TYPO3\CMS\Recordlist\Event\ModifyRecordListRecordActionsEvent
  • \TYPO3\CMS\Recordlist\Event\ModifyRecordListTableActionsEvent
  • \TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent
  • \TYPO3\CMS\Recordlist\LinkHandler\AbstractLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\FileLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\FolderLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface
  • \TYPO3\CMS\Recordlist\LinkHandler\MailLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\PageLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\TelephoneLinkHandler
  • \TYPO3\CMS\Recordlist\LinkHandler\UrlLinkHandler
  • \TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
  • \TYPO3\CMS\Recordlist\RecordList\DownloadRecordList
  • \TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface
  • \TYPO3\CMS\Recordlist\View\FolderUtilityRenderer
  • \TYPO3\CMS\Recordlist\View\RecordSearchBoxComponent

The following PHP class methods that have previously been marked as deprecated with v12 have been removed:

  • \TYPO3\CMS\Backend\Template\Components\ButtonBar->makeHelpButton()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->getBodyTag()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->getDynamicTabMenu()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->getView()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->header()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->isUiBlock()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->registerModuleMenu()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->renderContent()
  • \TYPO3\CMS\Backend\Template\ModuleTemplate->setContent()
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->addTagAttributes()
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->determineScriptUrl()
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->getRootIcon()
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->getRootRecord()
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->getThisScript()
  • \TYPO3\CMS\Core\Authentication\BackendUserAuthentication->modAccess()
  • \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools->removeElementTceFormsRecursive()
  • \TYPO3\CMS\Core\Database\Driver\PDOMySql\Driver->getName()
  • \TYPO3\CMS\Core\Database\Driver\PDOPgSql\Driver->getName()
  • \TYPO3\CMS\Core\Database\Driver\PDOSqlite\Driver->getName()
  • \TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression->add()
  • \TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression->addMultiple()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->andX()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->orX()
  • \TYPO3\CMS\Core\Database\Query\QueryBuilder->execute()
  • \TYPO3\CMS\Core\Domain\Repository\PageRepository->getExtURL()
  • \TYPO3\CMS\Core\Environment->getBackendPath()
  • \TYPO3\CMS\Core\Localization\LanguageService->getLL()
  • \TYPO3\CMS\Core\Localization\Locales->getIsoMapping()
  • \TYPO3\CMS\Core\Page\JavaScriptModuleInstruction->shallLoadRequireJs()
  • \TYPO3\CMS\Core\Page\PageRenderer->loadRequireJs()
  • \TYPO3\CMS\Core\Page\PageRenderer->loadRequireJsModule()
  • \TYPO3\CMS\Core\Page\PageRenderer->setRenderXhtml()
  • \TYPO3\CMS\Core\Page\PageRenderer->getRenderXhtml()
  • \TYPO3\CMS\Core\Page\PageRenderer->setCharSet()
  • \TYPO3\CMS\Core\Page\PageRenderer->getCharSet()
  • \TYPO3\CMS\Core\Page\PageRenderer->setMetaCharsetTag()
  • \TYPO3\CMS\Core\Page\PageRenderer->getMetaCharsetTag()
  • \TYPO3\CMS\Core\Page\PageRenderer->setBaseUrl()
  • \TYPO3\CMS\Core\Page\PageRenderer->getBaseUrl()
  • \TYPO3\CMS\Core\Page\PageRenderer->enableRemoveLineBreaksFromTemplate()
  • \TYPO3\CMS\Core\Page\PageRenderer->disableRemoveLineBreaksFromTemplate()
  • \TYPO3\CMS\Core\Page\PageRenderer->getRemoveLineBreaksFromTemplate()
  • \TYPO3\CMS\Core\Page\PageRenderer->enableDebugMode()
  • \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter->filterInlineChildren()
  • \TYPO3\CMS\Core\Session\UserSessionManager->createFromGlobalCookieOrAnonymous()
  • \TYPO3\CMS\Core\Site\Entity\SiteLanguage->getTwoLetterIsoCode()
  • \TYPO3\CMS\Core\Site\Entity\SiteLanguage->getDirection()
  • \TYPO3\CMS\Core\Type\DocType->getXhtmlDocType()
  • \TYPO3\CMS\Dashboard\DashboardInitializationService->getRequireJsModules()
  • \TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager->getContentObject()
  • \TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager->setContentObject()
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManager->getContentObject()
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManager->isFeatureEnabled()
  • \TYPO3\CMS\Extbase\Configuration\ConfigurationManager->setContentObject()
  • \TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager->getContentObject()
  • \TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager->setContentObject()
  • \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder->getRequest()
  • \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings->setLanguageOverlayMode()
  • \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings->getLanguageOverlayMode()
  • \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings->setLanguageUid()
  • \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings->getLanguageUid()
  • \TYPO3\CMS\Extbase\Property\AbstractTypeConverter->canConvertFrom()
  • \TYPO3\CMS\Extbase\Property\AbstractTypeConverter->getPriority()
  • \TYPO3\CMS\Extbase\Property\AbstractTypeConverter->getSupportedTargetType()
  • \TYPO3\CMS\Extbase\Property\AbstractTypeConverter->getSupportedSourceTypes()
  • \TYPO3\CMS\Fluid\View\StandaloneView->getFormat()
  • \TYPO3\CMS\Fluid\View\StandaloneView->getRequest()
  • \TYPO3\CMS\Fluid\View\StandaloneView->getTemplatePathAndFilename()
  • \TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent->getErrorMessage()
  • \TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent->isPropagationStopped()
  • \TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent->setAsInvalid()
  • \TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent->setHashedPassword()
  • \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication->getUserTSconf()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->baseUrlWrap()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->checkEnableFields()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->doWorkspacePreview()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPagesTSconfig()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->initUserGroups()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->isBackendUserLoggedIn()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->isUserOrGroupSet()
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->whichWorkspace()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->createFromUriString()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getATagParams()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getMailTo()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getQueryArguments()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getTreeList()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getTypoLink_URL()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getTypoLink()
  • \TYPO3\CMS\Frontend\Typolink\LinkFactory->getUrlToCurrentLocation()
  • \TYPO3\CMS\Scheduler\Scheduler->addTask()
  • \TYPO3\CMS\Scheduler\Scheduler->fetchTaskRecord()
  • \TYPO3\CMS\Scheduler\Scheduler->fetchTaskWithCondition()
  • \TYPO3\CMS\Scheduler\Scheduler->fetchTask()
  • \TYPO3\CMS\Scheduler\Scheduler->isValidTaskObject()
  • \TYPO3\CMS\Scheduler\Scheduler->removeTask()
  • \TYPO3\CMS\Scheduler\Scheduler->saveTask()
  • \TYPO3\CMS\Scheduler\Task\AbstractTask->isExecutionRunning()
  • \TYPO3\CMS\Scheduler\Task\AbstractTask->markExecution()
  • \TYPO3\CMS\Scheduler\Task\AbstractTask->remove()
  • \TYPO3\CMS\Scheduler\Task\AbstractTask->unmarkAllExecutions()
  • \TYPO3\CMS\Scheduler\Task\AbstractTask->unmarkExecution()
  • \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent->addModule()
  • \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent->getModules()

The following PHP static class methods that have previously been marked as deprecated for v12 have been removed:

  • \TYPO3\CMS\Backend\Utility\BackendUtility::ADMCMD_previewCmds()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::cshItem()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getClickMenuOnIconTagParameters()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getDropdownMenu()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getFuncCheck()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getFuncMenu()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getLinkToDataHandlerAction()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getPreviewUrl()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getRecordToolTip()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getThumbnailUrl()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignalCode()
  • \TYPO3\CMS\Backend\Utility\BackendUtility::isModuleSetInTBE_MODULES()
  • \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
  • \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::purgeInstances()
  • \TYPO3\CMS\Core\Page\JavaScriptModuleInstruction::forRequireJS()
  • \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::transform()
  • \TYPO3\CMS\Core\Utility\DebugUtility::debugInPopUpWindow()
  • \TYPO3\CMS\Core\Utility\DebugUtility::debugRows()
  • \TYPO3\CMS\Core\Utility\DebugUtility::printArray()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addCoreNavigationComponent()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addNavigationComponent()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig()
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::insertModuleFunction()
  • \TYPO3\CMS\Core\Utility\GeneralUtility::_GET()
  • \TYPO3\CMS\Core\Utility\GeneralUtility::_GP()
  • \TYPO3\CMS\Core\Utility\GeneralUtility::_GPmerged()
  • \TYPO3\CMS\Core\Utility\GeneralUtility::_POST()
  • \TYPO3\CMS\Core\Utility\GeneralUtility::linkThisScript()
  • \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule()
  • \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter()

The following methods changed signature according to previous deprecations in v12 at the end of the argument list:

  • \TYPO3\CMS\Backend\Form\FormDataCompiler->compile() (argument 2 is now required)
  • \TYPO3\CMS\Core\Messaging\AbstractMessage->setSeverity() (argument 1 is now of type ContextualFeedbackSeverity)
  • \TYPO3\CMS\Core\Messaging\FlashMessageQueue->clear() (argument 1 is now of type ContextualFeedbackSeverity|null)
  • \TYPO3\CMS\Core\Messaging\FlashMessageQueue->getAllMessagesAndFlush() (argument 1 is now of type ContextualFeedbackSeverity|null)
  • \TYPO3\CMS\Core\Messaging\FlashMessageQueue->getAllMessages() (argument 1 is now of type ContextualFeedbackSeverity|null)
  • \TYPO3\CMS\Core\Messaging\FlashMessageQueue->removeAllFlashMessagesFromSession() (argument 1 is now of type ContextualFeedbackSeverity|null)
  • \TYPO3\CMS\Core\Messaging\FlashMessages->__construct() (argument 3 is now of type ContextualFeedbackSeverity)
  • \TYPO3\CMS\Core\Page\PageRenderer->setLanguage() (argument 1 is now of type Locale)
  • \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility->addMessageToFlashMessageQueue() (argument 2 is now of type ContextualFeedbackSeverity|null)
  • \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode() (argument 4 $limit has been removed)
  • \TYPO3\CMS\Extbase\Mvc\Controller\ActionController->addFlashMessage() (argument 2 is now of type ContextualFeedbackSeverity)
  • \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate() (argument 4 has been removed)
  • \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->start() (argument 3 $request has been removed)
  • \TYPO3\CMS\Reports\Status->__construct() (argument 4 is now of type ContextualFeedbackSeverity)
  • \TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider->addMessage() (argument 2 is now of type ContextualFeedbackSeverity)

The following public class properties have been dropped:

  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->BE_USER
  • \TYPO3\CMS\Backend\Tree\View\AbstractTreeView->thisScript
  • \TYPO3\CMS\Core\Localization\LanguageService->debugKey
  • \TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce->b64
  • \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->lastTypoLinkLD
  • \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->lastTypoLinkTarget
  • \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->lastTypoLinkUrl
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->baseUrl
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->extTarget
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->fileTarget
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->intTarget
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->spamProtectEmailAddresses
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->tmpl
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->xhtmlDoctype
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->xhtmlVersion

The following class method visibility has been changed to protected:

  • \TYPO3\CMS\Core\Domain\Repository\PageRepository->getRecordOverlay()

The following class methods are now marked as internal:

  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->isSetSessionCookie()
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->isRefreshTimeBasedCookie()
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->removeCookie()
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->isCookieSet()
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->unpack_uc()
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->appendCookieToResponse()

The following class methods now have a native return type and removed the #[\ReturnTypeWillChange] attribute:

  • \TYPO3\CMS\Core\Collection\AbstractRecordCollection->current()
  • \TYPO3\CMS\Core\Collection\AbstractRecordCollection->key()
  • \TYPO3\CMS\Core\Log\LogRecord->offsetGet()
  • \TYPO3\CMS\Core\Messaging\FlashMessageQueue->dequeue()
  • \TYPO3\CMS\Core\Resource\Collection\AbstractFileCollection->key()
  • \TYPO3\CMS\Core\Resource\MetaDataAspect->offsetGet()
  • \TYPO3\CMS\Core\Resource\MetaDataAspect->current()
  • \TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult->current()
  • \TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult->key()
  • \TYPO3\CMS\Core\Routing\SiteRouteResult->offsetGet()
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy->current()
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy->key()
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage->current()
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage->offsetGet()
  • \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult->offsetGet()
  • \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult->current()
  • \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult->key()
  • \TYPO3\CMS\Extbase\Persistence\ObjectStorage->current()
  • \TYPO3\CMS\Extbase\Persistence\ObjectStorage->offsetGet()
  • \TYPO3\CMS\Filelist\Dto\ResourceCollection->current()
  • \TYPO3\CMS\Filelist\Dto\ResourceCollection->key()

The following class properties visibility have been changed to protected:

  • \TYPO3\CMS\Core\Domain\Repository\PageRepository->where_hid_del
  • \TYPO3\CMS\Core\Domain\Repository\PageRepository->where_groupAccess
  • \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->type

The following class property visibility has been changed to private:

  • \TYPO3\CMS\Core\Type\DocType->getXhtmlVersion

The following class properties have been marked as internal:

  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->lastLogin_column
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->formfield_uname
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->formfield_uident
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->formfield_status
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->loginSessionStarted
  • \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication->dontSetCookie
  • \TYPO3\CMS\Core\Authentication\FrontendUserAuthentication->formfield_permanent
  • \TYPO3\CMS\Core\Authentication\FrontendUserAuthentication->is_permanent

The following class property has changed/enforced type:

  • \TYPO3\CMS\Core\Page\PageRenderer->endingSlash (is now string)

The following eID entry point has been removed:

  • requirejs

The following ViewHelpers have been changed or removed:

  • <f:be.buttons.csh> removed
  • <f:be.labels.csh> removed
  • <f:translate> Argument "alternativeLanguageKeys" has been removed

The following TypoScript options have been dropped or adapted:

  • config.baseURL
  • config.removePageCss
  • config.spamProtectEmailAddresses (only ascii value)
  • config.xhtmlDoctype
  • plugin.[pluginName]._CSS_PAGE_STYLE
  • [ip()] condition function must be used in a context with request
  • [loginUser()] condition function removed
  • [usergroup()] condition function removed
  • constants setup top-level-object and constants property of parseFunc
  • plugin.tx_felogin_login.settings.passwordValidators has been removed

The following constant has been dropped:

  • TYPO3_mainDir

The following class constants have been dropped:

  • \TYPO3\CMS\Core\Messaging\AbstractMessage::ERROR
  • \TYPO3\CMS\Core\Messaging\AbstractMessage::INFO
  • \TYPO3\CMS\Core\Messaging\AbstractMessage::NOTICE
  • \TYPO3\CMS\Core\Messaging\AbstractMessage::OK
  • \TYPO3\CMS\Core\Messaging\AbstractMessage::WARNING
  • \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR
  • \TYPO3\CMS\Core\Messaging\FlashMessage::INFO
  • \TYPO3\CMS\Core\Messaging\FlashMessage::NOTICE
  • \TYPO3\CMS\Core\Messaging\FlashMessage::OK
  • \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING
  • \TYPO3\CMS\Core\Page\JavaScriptModuleInstruction::FLAG_LOAD_REQUIRE_JS
  • \TYPO3\CMS\Reports\Status::ERROR
  • \TYPO3\CMS\Reports\Status::INFO
  • \TYPO3\CMS\Reports\Status::NOTICE
  • \TYPO3\CMS\Reports\Status::OK
  • \TYPO3\CMS\Reports\Status::WARNING

The following global option handling have been dropped and are ignored:

  • $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig']
  • $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] only accepts a boolean value now

The following global variables have been removed:

  • $GLOBALS['TBE_STYLES']
  • $GLOBALS['TBE_STYLES']['stylesheet']
  • $GLOBALS['TBE_STYLES']['stylesheet2']
  • $GLOBALS['TBE_STYLES']['skins']
  • $GLOBALS['TBE_STYLES']['admPanel']
  • $GLOBALS['TCA_DESCR']

The following hooks have been removed:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/cache/frontend/class.t3lib_cache_frontend_abstractfrontend.php']['flushByTag']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Lowlevel\Controller\ConfigurationController']['modifyBlindedConfigurationOptions']

The following single field configuration has been removed from TCA:

  • MM_insert_fields (for TCA fields with MM configuration)

The following event has been removed:

  • \TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent

The following fallbacks have been removed:

  • Usage of the ext_icon.* file locations for extension icons
  • Usage of the result property additionalJavaScriptPost of the form engine result array
  • Using chart.js v3 compatible widgets in ext:dashboard
  • Usage of .t3js-contextmenutrigger to trigger and configure context menus
  • Usage of the jsonArray property scriptCall for AjaxController's
  • Binding the selected menu items to callback actions in context menus
  • Checking for \TYPO3\CMS\Core\Site\SiteLanguageAwareTrait is removed in \TYPO3\CMS\Core\Routing\Aspect\AspectFactory
  • f:format.html ViewHelper no longer works in BE context
  • Usage of JScode containing inline JavaScript for handing custom signals
  • Usage property $resultArray['requireJsModules'] of the form engine result array
  • Using backend FormEngine, the current ServerRequestInterface request must be provided in key "request" as initialData to FormDataCompiler, the fallback to $GLOBALS['TYPO3_REQUEST'] has been removed.
  • Compatibility layer for "TCEforms" key in FlexFormTools has been removed
  • Compatibility layer for using array parameters for files in extbase (use UploadedFile instead)

The following upgrade wizards have been removed:

  • Wizard for migrating backend user languages
  • Wizard for installing the extension "legacy_collections" from TER
  • Wizard for migrating the transOrigDiffSourceField field to a json encoded string
  • Wizard for cleaning up workspace new placeholders
  • Wizard for cleaning up workspace move placeholders
  • Wizard for migrating shortcut records
  • Wizard for sanitizing existing SVG files in the fileadmin folder
  • Wizard for populating a new channel column of the sys_log table

The following features are now always enabled:

  • security.backend.enforceContentSecurityPolicy

The following feature has been removed:

  • Regular expression based validators in ext:form backend UI

The following database table fields have been removed:

  • fe_users.TSconfig
  • fe_groups.TSconfig

The following backend route identifier has been removed:

  • ajax_core_requirejs

The following global JavaScript variable has been removed:

  • TYPO3.Tooltip

The following global JavaScript function has been removed:

  • Global_JavaScript_Function_Name

The following JavaScript module has been removed:

  • tooltip

The following JavaScript method behaviour has changed:

  • ColorPicker.initialize() always requires an HTMLInputElement to be passed as first argument

The following JavaScript method has been removed:

  • getParameterFromUrl() of @typo3/backend/utility

The following CKEditor plugin has been removed:

  • SoftHyphen

The following dependency injection service aliase has been removed:

  • @dashboard.views.widget

Impact 

Using above removed functionality will most likely raise PHP fatal level errors, may change website output or crashes browser JavaScript.

Breaking: #100966 - Remove jquery-ui 

See forge#100966

Description 

The NPM package jquery-ui has been removed completely for TYPO3 v13 without any substitute.

According to the TYPO3 Deprecation Policy, JavaScript code and packages used only in the TYPO3 backend are not considered to be part of that policy:

The deprecation policy does not cover the deprecations of backend components such as JavaScript code, CSS code, HTML code, and backend templates.

Impact 

TYPO3 does not ship the NPM package jquery-ui any longer. Third-party extensions that rely on this package will be broken and need to be adjusted.

Since TYPO3 exposed only parts of jquery-ui, only the components core, draggable, droppable, mouse, resizable, selectable, sortable and widget are affected - other components simply did not exist.

Affected installations 

Those having custom or third-party extensions using jquery-ui from typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery-ui/.

Migration 

TYPO3 does not provide any substitute. In TYPO3 the draggable and resizable features of jquery-ui have been reimplemented in the new custom element <typo3-backend-draggable-resizable>.

Breaking: #101129 - Convert Action to native backed enum 

See forge#101129

Description 

The class \TYPO3\CMS\Scheduler\Task\Enumeration\Action is now converted to a native backed enum. In addition the class is moved to the namespace \TYPO3\CMS\Scheduler and renamed to SchedulerManagementAction.

Impact 

Since \TYPO3\CMS\Scheduler\Task\Enumeration\Action is no longer a class, the existing class constants are no longer available. In addition it's not possible to instantiate it anymore.

Affected installations 

Third-party extensions using the following class constants:

  • \TYPO3\CMS\Scheduler\Task\Enumeration\Action::ADD
  • \TYPO3\CMS\Scheduler\Task\Enumeration\Action::EDIT
  • \TYPO3\CMS\Scheduler\Task\Enumeration\Action::LIST

Class instantiation:

  • new Action('a-string')

Migration 

Include the enum SchedulerManagementAction from namespace \TYPO3\CMS\Scheduler as a replacement for Action.

Use the new syntax

  • \TYPO3\CMS\Scheduler\SchedulerManagementAction::ADD
  • \TYPO3\CMS\Scheduler\SchedulerManagementAction::EDIT
  • \TYPO3\CMS\Scheduler\SchedulerManagementAction::LIST

as well as the tryFrom($aString) static method of the backed enum.

Breaking: #101131 - Convert LoginType to native backed enum 

See forge#101131

Description 

The class \TYPO3\CMS\Core\Authentication\LoginType is now converted to a native backed enum.

Impact 

Since \TYPO3\CMS\Core\Authentication\LoginType is no longer a class, the existing class constants are no longer available.

Affected installations 

Custom authenticators using the following class constants:

  • \TYPO3\CMS\Core\Authentication\LoginType::LOGIN
  • \TYPO3\CMS\Core\Authentication\LoginType::LOGOUT

Migration 

Use the new syntax: \TYPO3\CMS\Core\Authentication\LoginType::LOGIN->value \TYPO3\CMS\Core\Authentication\LoginType::LOGOUT->value

Alternatively, use the enum method tryFrom to convert a value to an enum. For direct comparison of two enums, the null-coalescing operator shall be used to ensure that the parameter is a string:

<?php

use TYPO3\CMS\Core\Authentication\LoginType;

if (LoginType::tryFrom($value ?? '') === LoginType::LOGIN) {
    // Do login stuff
}
if (LoginType::tryFrom($value ?? '') === LoginType::LOGOUT) {
    // Do logout stuff
}
Copied!

Breaking: #101133 - IconFactory->getIcon() signature change 

See forge#101133

Description 

The public method getIcon() in \TYPO3\CMS\Core\Imaging\IconFactory has changed its 4th parameter, in order to prepare the removal of class \TYPO3\CMS\Core\Type\Icon\IconState.

Impact 

Custom extensions extending the getIcon() method of class \TYPO3\CMS\Core\Imaging\IconFactory not having the same signature will fail with a PHP fatal error.

Affected installations 

Custom extensions extending the getIcon() method from class \TYPO3\CMS\Core\Imaging\IconFactory .

Migration 

Adapt the 4th parameter of getIcon() to be of type \TYPO3\CMS\Core\Type\Icon\IconState|IconState $state = null

In addition, adapt the code in the body of the method.

Breaking: #101133 - Icon->state changed type 

See forge#101133

Description 

The protected property \TYPO3\CMS\Core\Imaging\Icon->state holds now a native enum \TYPO3\CMS\Core\Imaging\IconState instead of an instance of \TYPO3\CMS\Core\Type\Icon\IconState.

Impact 

Custom extensions calling \TYPO3\CMS\Core\Imaging\Icon->getState() will receive an enum now, which will most probably lead to PHP errors in the runtime.

Custom extensions calling \TYPO3\CMS\Core\Imaging\Icon->setState() with an instance of \TYPO3\CMS\Core\Type\Icon\IconState will receive a PHP TypeError.

Affected installations 

Custom extensions calling \TYPO3\CMS\Core\Imaging\Icon->getState() or \TYPO3\CMS\Core\Imaging\Icon->setState().

Migration 

Adapt your code to handle the native enum \TYPO3\CMS\Core\Imaging\IconState .

use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Type\Icon\IconState;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before
$icon = GeneralUtility::makeInstance(Icon::class);
$icon->setState(IconState::cast(IconState::STATE_DEFAULT));
$state = $icon->getState();
$stateValue = (string)$state;

// After
$icon = GeneralUtility::makeInstance(Icon::class);
$icon->setState(IconState::STATE_DEFAULT);

$state = $icon->getState();
$stateValue = $state->value;
Copied!

Breaking: #101137 - Page Doktype "Recycler" removed 

See forge#101137

Description 

TYPO3 had multiple concepts of a recycler / trash bin. One of the oldest concepts was the ability to create a manual page of the type "Recycler" (page records with doktype=255 set) where editors could manually move content to such a page instead of deleting it. One other option is to use the Web > Recycler backend module (available with the shipped recycler system extension). This process is much more user-friendly: Any kind of record which has been (soft-)deleted can be viewed and re-added via this module, no manual process during the deletion process is needed.

For reasons of consistency and de-cluttering the UI, the former functionality has been removed from TYPO3 Core, along with the PHP class constant \TYPO3\CMS\Domain\Repository\PageRepository::DOKTYPE_RECYCLER.

Impact 

The recycler doktype has been removed and cannot be selected or used anymore. Any existing recycler pages are migrated to a page of type "Backend User Section" which is also not accessible, if there is no valid backend user with permission to see this page.

Affected installations 

TYPO3 installations using this special page doktype "Recycler".

Migration 

A migration is in place, it is recommended to use the Recycler module with soft-deleting records.

Breaking: #101143 - Strict typing in LinktypeInterface 

See forge#101143

Description 

All methods in the interface \TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface are now strictly typed.

Impact 

Classes implementing the interface must now ensure all methods are strictly typed.

Affected installations 

Custom classes implementing \TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface

Migration 

Ensure that classes that implement \TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface have the following signatures:

public function checkLink(string $url, array $softRefEntry, LinkAnalyzer $reference): bool;
public function fetchType(array $value, string $type, string $key): string;
public function getErrorParams(): array;
public function getBrokenUrl(array $row): string;
public function getErrorMessage(array $errorParams): string;
Copied!

Breaking: #101149 - Mark PageTsBackendLayoutDataProvider as final 

See forge#101149

Description 

The class \TYPO3\CMS\Backend\View\BackendLayout\PageTsBackendLayoutDataProvider is marked as final.

Impact 

It is no longer possible to extend the class \TYPO3\CMS\Backend\View\BackendLayout\PageTsBackendLayoutDataProvider .

Affected installations 

Classes extending \TYPO3\CMS\Backend\View\BackendLayout\PageTsBackendLayoutDataProvider .

Migration 

Instead of extending the data provider, it is recommended to register a custom DataProvider for backend layouts, which can already be used since TYPO3 v7.

Breaking: #101175 - Convert VersionState to native backed enum 

See forge#101175

Description 

The class \TYPO3\CMS\Core\Versioning\VersionState is now converted to a native PHP backed enum.

Impact 

Since \TYPO3\CMS\Core\Versioning\VersionState is no longer a class, the existing class constants are no longer available, but are enum instances instead.

In addition it's not possible to instantiate it anymore or call the equals() method.

Affected installations 

TYPO3 code using the following code:

Using the following class constants:

  • \TYPO3\CMS\Core\Versioning\VersionState::DEFAULT_STATE
  • \TYPO3\CMS\Core\Versioning\VersionState::NEW_PLACEHOLDER
  • \TYPO3\CMS\Core\Versioning\VersionState::DELETE_PLACEHOLDER
  • \TYPO3\CMS\Core\Versioning\VersionState::MOVE_POINTER

Class instantiation:

  • new \TYPO3\CMS\Core\Versioning\(VersionState::*->value)

where * denotes one of the enum values.

Method invocation:

  • \TYPO3\CMS\Core\Versioning\VersionState::cast()
  • \TYPO3\CMS\Core\Versioning\VersionState::cast()->equals()

Migration 

Use the new syntax for getting the values:

  • \TYPO3\CMS\Core\Versioning\VersionState::DEFAULT_STATE->value
  • \TYPO3\CMS\Core\Versioning\VersionState::NEW_PLACEHOLDER->value
  • \TYPO3\CMS\Core\Versioning\VersionState::DELETE_PLACEHOLDER->value
  • \TYPO3\CMS\Core\Versioning\VersionState::MOVE_POINTER->value

Class instantiation should be replaced by:

  • \TYPO3\CMS\Core\Versioning\VersionState::tryFrom($row['t3ver_state'])

Method invocation of cast()/ equals() should be replaced by:

  • \TYPO3\CMS\Core\Versioning\VersionState::tryFrom(...)
  • \TYPO3\CMS\Core\Versioning\VersionState::tryFrom(...) === VersionState::MOVE_POINTER

Breaking: #101186 - Strict typing in UnableToLinkException 

See forge#101186

Description 

The class constructor in \TYPO3\CMS\Frontend\Exception\UnableToLinkException is now strictly typed. In addition, the variable $linkText has type string.

Impact 

The class constructor is now strictly typed.

Affected installations 

TYPO3 sites using the \TYPO3\CMS\Frontend\Exception\UnableToLinkException exception.

Migration 

Ensure that the class constructor is called properly, according to the changed signature:

public function __construct(
    string $message = '',
    int $code = 0,
    ?\Throwable $previous = null,
    string $linkText = ''
);
Copied!

Breaking: #101192 - Remove fallback for CKEditor removePlugins 

See forge#101192

Description 

Remove fallback for CKEditor configuration removePlugins as a string.

Impact 

Runtime Javascript errors can occur if the CKEditor configuration removePlugins isn't an array.

Affected installations 

TYPO3 installation which have CKEditor configuration removePlugins configured as a string.

Migration 

Adjust your CKEditor configuration and pass removePlugins as array.

Before 

editor:
    config:
        removePlugins: image
Copied!

After 

editor:
    config:
        removePlugins:
            - image
Copied!

Breaking: #101266 - Remove RequireJS 

See forge#101266

Description 

The RequireJS project has been discontinued and was therefore deprecated in TYPO3 v12 with forge#96510 in favor of native ECMAScript v6/v11 modules (added in forge#96510).

The infrastructure for configuration and loading of RequireJS modules is now removed.

Impact 

Registering FormEngine JavaScript modules via 'requireJsModules' will have no effect. The PageRenderer endpoints \TYPO3\CMS\Core\Page\PageRenderer->loadRequireJs() and \TYPO3\CMS\Core\Page\PageRenderer->loadRequireJsModule() have been removed and must no longer be called. The respective includeJavaScriptModules property of the ViewHelper <f:be.pageRenderer> ViewHelper has also been removed.

Affected installations 

TYPO3 installations using RequireJS modules to provide JavaScript in the TYPO3 backend, or – less common – use PageRenderer RequireJS infrastructure for frontend JavaScript module loading.

Migration 

Migrate your JavaScript from the AMD module format to native ES6 modules and register your configuration in Configuration/JavaScriptModules.php, also see forge#96510 and ES6 in the TYPO3 Backend for more information:

# Configuration/JavaScriptModules.php
<?php

return [
    'dependencies' => ['core', 'backend'],
    'imports' => [
        '@vendor/my-extension/' => 'EXT:my_extension/Resources/Public/JavaScript/',
    ],
];
Copied!

Then use \TYPO3\CMS\Core\Page\PageRenderer->loadJavaScriptModule() instead of \TYPO3\CMS\Core\Page\PageRenderer->loadRequireJsModule() to load the ES6 module:

// via PageRenderer
$this->pageRenderer->loadJavaScriptModule('@vendor/my-extension/example.js');
Copied!

In Fluid templates includeJavaScriptModules is to be used instead of includeRequireJsModules:

In Fluid template the includeJavaScriptModules property of the <f:be.pageRenderer> ViewHelper may be used:

<f:be.pageRenderer
   includeJavaScriptModules="{
      0: '@vendor/my-extension/example.js'
   }"
/>
Copied!

Breaking: #101281 - Introduce type declarations in ResourceInterface 

See forge#101281

Description 

The following methods of interface \TYPO3\CMS\Core\Resource\ResourceInterface have been given return type declarations:

public function getIdentifier(): string;
public function getName(): string;
public function getStorage(): ResourceStorage;
public function getHashedIdentifier(): string;
public function getParentFolder(): FolderInterface;
Copied!

Impact 

This affects many classes due to the following implementation rules:

  • \TYPO3\CMS\Core\Resource\Folder , because it implements \TYPO3\CMS\Core\Resource\FolderInterface which extends \TYPO3\CMS\Core\Resource\ResourceInterface
  • \TYPO3\CMS\Core\Resource\FileReference , and \TYPO3\CMS\Core\Resource\AbstractFile because both implement \TYPO3\CMS\Core\Resource\FileInterface which extends \TYPO3\CMS\Core\Resource\ResourceInterface
  • \TYPO3\CMS\Core\Resource\File and \TYPO3\CMS\Core\Resource\ProcessedFile because both extend \TYPO3\CMS\Core\Resource\AbstractFile

In consequence, the following methods are affected:

  • \TYPO3\CMS\Core\Resource\Folder::getIdentifier()
  • \TYPO3\CMS\Core\Resource\Folder::getName()
  • \TYPO3\CMS\Core\Resource\Folder::getStorage()
  • \TYPO3\CMS\Core\Resource\Folder::getHashedIdentifier()
  • \TYPO3\CMS\Core\Resource\Folder::getParentFolder()
  • \TYPO3\CMS\Core\Resource\FileReference::getIdentifier()
  • \TYPO3\CMS\Core\Resource\FileReference::getName()
  • \TYPO3\CMS\Core\Resource\FileReference::getStorage()
  • \TYPO3\CMS\Core\Resource\FileReference::getHashedIdentifier()
  • \TYPO3\CMS\Core\Resource\FileReference::getParentFolder()
  • \TYPO3\CMS\Core\Resource\AbstractFile::getIdentifier()
  • \TYPO3\CMS\Core\Resource\AbstractFile::getName()
  • \TYPO3\CMS\Core\Resource\AbstractFile::getStorage()
  • \TYPO3\CMS\Core\Resource\AbstractFile::getHashedIdentifier()
  • \TYPO3\CMS\Core\Resource\AbstractFile::getParentFolder()
  • \TYPO3\CMS\Core\Resource\File::getIdentifier()
  • \TYPO3\CMS\Core\Resource\File::getName()
  • \TYPO3\CMS\Core\Resource\File::getStorage()
  • \TYPO3\CMS\Core\Resource\File::getHashedIdentifier()
  • \TYPO3\CMS\Core\Resource\File::getParentFolder()
  • \TYPO3\CMS\Core\Resource\ProcessedFile::getIdentifier()
  • \TYPO3\CMS\Core\Resource\ProcessedFile::getName()
  • \TYPO3\CMS\Core\Resource\ProcessedFile::getStorage()
  • \TYPO3\CMS\Core\Resource\ProcessedFile::getHashedIdentifier()
  • \TYPO3\CMS\Core\Resource\ProcessedFile::getParentFolder()

Affected installations 

Affected installations are those which either implement the ResourceInterface directly (very unlikely) or those that extend any of mentioned implementations (Core classes).

The usage (the API) of those implementation itself has not changed!

Migration 

Use the same return type declarations as ResourceInterface does.

Breaking: #101291 - Introduce capabilities bit set 

See forge#101291

Description 

The capabilities property of the ResourceStorage and drivers ( LocalDriver/ AbstractDriver) have been converted from an integer (holding a bit value) to an instance of a new BitSet class \TYPO3\CMS\Core\Resource\Capabilities .

This affects the public API of the following interface methods:

  • \TYPO3\CMS\Core\Resource\Driver\DriverInterface::getCapabilities()
  • \TYPO3\CMS\Core\Resource\Driver\DriverInterface::mergeConfigurationCapabilities()

In consequence, all mentioned methods of implementations are affected as well, those of:

  • \TYPO3\CMS\Core\Resource\Driver\AbstractDriver::getCapabilities()
  • \TYPO3\CMS\Core\Resource\Driver\LocalDriver::mergeConfigurationCapabilities()

Also the following constants have been removed:

  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_BROWSABLE
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_PUBLIC
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_WRITABLE
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS

Impact 

The return type of the following methods, respective their implementations have changed from int to \TYPO3\CMS\Core\Resource\Capabilities :

  • \TYPO3\CMS\Core\Resource\Driver\DriverInterface::getCapabilities()
  • \TYPO3\CMS\Core\Resource\Driver\DriverInterface::mergeConfigurationCapabilities()

The type of the parameter $capabilities of the method mergeConfigurationCapabilities() has been changed from int to \TYPO3\CMS\Core\Resource\Capabilities .

The usage of the mentioned, removed constants of \TYPO3\CMS\Core\Resource\ResourceStorageInterface will lead to errors.

Affected installations 

Installations that implement custom drivers and therefore directly implement \TYPO3\CMS\Core\Resource\Driver\DriverInterface or extend \TYPO3\CMS\Core\Resource\Driver\AbstractDriver .

Also, installations that use the removed constants of \TYPO3\CMS\Core\Resource\ResourceStorageInterface .

Migration 

When using mentioned methods that formerly returned the bit value as integer or expected the bit value as integer parameter need to use the Capabilities class instead. It behaves exactly the same as the plain integer. If the plain integer value needs to be retrieved, __toInt() can be called on Capabilities instances.

The following removed constants

  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_BROWSABLE
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_PUBLIC
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_WRITABLE
  • \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS

can be replaced with public constants of the new Capabilities class:

  • \TYPO3\CMS\Core\Resource\Capabilities::CAPABILITY_BROWSABLE
  • \TYPO3\CMS\Core\Resource\Capabilities::CAPABILITY_PUBLIC
  • \TYPO3\CMS\Core\Resource\Capabilities::CAPABILITY_WRITABLE
  • \TYPO3\CMS\Core\Resource\Capabilities::CAPABILITY_HIERARCHICAL_IDENTIFIERS

Breaking: #101294 - Introduce type declarations in FileInterface 

See forge#101294

Description 

Return and param type declarations have been introduced for all methods stubs of \TYPO3\CMS\Core\Resource\FileInterface .

Impact 

In consequence, all implementations of \TYPO3\CMS\Core\Resource\FileInterface need to reflect those changes and add the same return and param type declarations.

In case, any of the Core implementations are extended, overridden methods might need to be adjusted. The Core classes, implementing \TYPO3\CMS\Core\Resource\FileInterface , are:

  • \TYPO3\CMS\Core\Resource\AbstractFile
  • \TYPO3\CMS\Core\Resource\File
  • \TYPO3\CMS\Core\Resource\FileReference
  • \TYPO3\CMS\Core\Resource\ProcessedFile

Affected installations 

Only those installations that implement \TYPO3\CMS\Core\Resource\FileInterface directly or that extend any of those mentioned core implementations.

Migration 

Return and param type declarations have to be synced with the ones of the interface.

Breaking: #101305 - Introduce type declarations for some methods in GeneralUtility 

See forge#101305, forge#101453

Description 

Native return and param type declarations have been introduced for the following methods of \TYPO3\CMS\Core\Utility\GeneralUtility :

  • addInstance()
  • array2xml()
  • callUserFunction()
  • cmpFQDN()
  • cmpIP()
  • cmpIPv4()
  • cmpIPv6()
  • copyDirectory()
  • createDirectoryPath()
  • createVersionNumberedFilename()
  • explodeUrl2Array()
  • fixPermissions()
  • flushInternalRuntimeCaches()
  • getAllFilesAndFoldersInPath()
  • getBytesFromSizeMeasurement()
  • getClassName()
  • getFileAbsFileName()
  • getFilesInDir()
  • getIndpEnv()
  • getInstances()
  • getLogger()
  • getSingletonInstances()
  • getUrl()
  • get_dirs()
  • get_tag_attributes()
  • implodeArrayForUrl()
  • implodeAttributes()
  • intExplode()
  • isAllowedAbsPath()
  • isOnCurrentHost()
  • isValidUrl()
  • jsonEncodeForHtmlAttribute()
  • jsonEncodeForJavaScript()
  • locationHeaderUrl()
  • makeInstanceForDi()
  • mkdir_deep()
  • mkdir()
  • normalizeIPv6()
  • purgeInstances()
  • quoteJSvalue()
  • removePrefixPathFromList()
  • removeSingletonInstance()
  • resetSingletonInstances()
  • resolveBackPath()
  • revExplode()
  • rmdir()
  • sanitizeLocalUrl()
  • setIndpEnv()
  • setSingletonInstance()
  • split_tag_attributes()
  • tempnam()
  • trimExplode()
  • unlink_tempfile()
  • upload_copy_move()
  • upload_to_tempfile()
  • validEmail()
  • validIP()
  • validIPv4()
  • validIPv6()
  • validPathStr()
  • webserverUsesHttps()
  • wrapJS()