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

Fri, 04 Sep 2026 22:05:17 +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 v15 

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

Also available 

15.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: #109153 - Obsolete pages-only new record wizard removed 

See forge#109153

Description 

The page creation wizard introduced with forge#108915 replaced the legacy "pages only" position selection flow of the records module. Its now unused predecessors have been removed:

  • The backend route db_new_pages (path /record/new-page).
  • The page TSconfig option mod.wizards.newRecord.pages.show.pageSelectPosition .

Impact 

Linking to the db_new_pages route raises an error. The page TSconfig option mod.wizards.newRecord.pages.show.pageSelectPosition no longer has any effect.

Affected installations 

Instances with third-party extensions that link to the db_new_pages route, or set the pageSelectPosition page TSconfig option. Such extensions are quite rare, and a left over page TSconfig shouldn't harm.

Migration 

Use the page creation wizard instead. New pages are created through the db_new route and the typo3-backend-new-page-wizard-button component, which guide position and page type selection. No replacement for PagePositionMap or the pageSelectPosition option is needed.

Breaking: #109783 - Deprecated functionality removed 

See forge#109783

Description 

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

The following PHP methods have been set to private and can no longer be called from outside the class:

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

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

The following PSR-14 events that have previously been marked as deprecated with v14 have been removed:

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

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

The following methods changed signature according to previous deprecations in v14:

  • \TYPO3\CMS\Core\Page\PageRenderer->render() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Page\PageRenderer->renderResponse() - argument $request is now mandatory and the first argument. The transitional ServerRequestInterface|int $requestOrCode union has been removed (Deprecation entry)
  • \TYPO3\CMS\Core\Page\PageRenderer->setDocType() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Page\PageRenderer->setLanguage() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Utility\GeneralUtility::sanitizeLocalUrl() - argument $request is now mandatory (Deprecation entry)
  • \TYPO3\CMS\Extbase\Attribute\ORM\Cascade->__construct() - argument $value is now a ?string (Deprecation entry)
  • \TYPO3\CMS\Extbase\Attribute\IgnoreValidation->__construct() - accepts no arguments any more (Deprecation entry)
  • \TYPO3\CMS\Extbase\Attribute\Validate->__construct() - argument $validator is not a string , argument $param has been removed (Deprecation entry)
  • \TYPO3\CMS\Filelist\FileList->start() - argument $sortDirection no longer accepts a bool , a \TYPO3\CMS\Filelist\Type\SortDirection enum is now required (Deprecation entry)

The following public class properties have been dropped:

The following protected class properties have been dropped:

  • \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder->contentObjectRenderer (Deprecation entry)
  • \TYPO3\CMS\Backend\ElementBrowser\AbstractElementBrowser->bparams . The legacy pipe-delimited bparams element browser request parameter is no longer evaluated. FormEngine now passes the individual fieldReference , allowedTypes and further parameters, which are handled by the typed \TYPO3\CMS\Backend\ElementBrowser\ElementBrowserParameters . Its fromBparams() and toBparams() conversion methods have been removed as well.

The following class constants have been dropped:

The following TypoScript options have been dropped or adapted:

The following user TSconfig options have been removed:

The following form yaml configurations that have previously been marked as deprecated for v14 have been removed:

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

  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][/*...*/]['tableoptions'] (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][/*...*/]['defaultTableOptions']['collate'] (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['fallbackToLegacyHash'] ; the transitional fallback to the legacy md5-based cHash validation has been removed, only the HMAC-SHA3 cHash is accepted (Breaking entry)
  • $GLOBALS['TYPO3_USER_SETTINGS'] ; backend user profile settings are now configured via TCA (the be_users user_settings column) using ExtensionManagementUtility::addUserSetting() (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths'] ; additional public folders are now exposed via resource definitions instead (Deprecation entry)

The following extension file loading has been removed:

  • ext_tables.php files in extensions are no longer considered during bootstrap (Deprecation entry)
  • ext_emconf.php files in extensions are no longer evaluated for package metadata. In TYPO3 classic (non-Composer) mode the composer.json must declare the extension version and the providesPackages definition in the extra/typo3/cms section. These two fields are now mandatory, also for extensions that no longer ship an ext_emconf.php, otherwise loading the extension throws an exception. (Deprecation entry)

The following TCA options are not evaluated anymore:

  • passwordRules option of the passwordGenerator field control; use passwordPolicy instead (Deprecation entry)

The following extbase attribute usages have been removed:

The following fallbacks have been removed:

  • \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->getRequest() no longer falls back to $GLOBALS['TYPO3_REQUEST'] ; code must call setRequest() after instantiation (Deprecation entry)
  • Page layout content area columns without an identifier no longer fall back to a generated hash based on the page layout identifier and colPos ; a missing identifier now throws a \RuntimeException (Feature introduction)
  • Manually creating and adding a \TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton to the button bar is no longer detected and no longer suppresses the automatic shortcut button; controllers must use \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent->setShortcutContext() instead (Deprecation entry)
  • A legacy typo3conf/LocalConfiguration.php and typo3conf/AdditionalConfiguration.php are no longer automatically migrated to config/system/settings.php and config/system/additional.php on first request. The configuration files have to reside at their final location. (Breaking entry)
  • The redis cache backend no longer accepts an array for the password option as a workaround to configure a username and password at once. Use the separate username and password options instead. (Deprecation entry)
  • The redis session backend no longer accepts an array for the password option as a workaround to configure a username and password at once. Use the separate username and password options instead. (Deprecation entry)
  • Flex form pageTsConfig ( TCEFORM ) and exclude-field addressing no longer resolves comma-separated dataStructureKey values (the legacy list_type,CType form); the data structure key is used as-is (Breaking entry)

The following upgrade wizards have been removed:

  • \TYPO3\CMS\Core\Upgrades\SysFileMimeTypeMigration (identifier sysFileMimeTypeMigration)
  • \TYPO3\CMS\Core\Upgrades\PagesRecyclerDoktypeMigration (identifier pagesRecyclerDoktypeMigration)
  • \TYPO3\CMS\Core\Upgrades\NullToDefaultUpdateWizard (identifier nullToDefaultUpdateWizard)
  • \TYPO3\CMS\Frontend\Upgrades\SynchronizeColPosAndCTypeWithDefaultLanguage (identifier synchronizeColPosAndCTypeWithDefaultLanguage)
  • \TYPO3\CMS\IndexedSearch\Upgrades\IndexedSearchCTypeMigration (identifier indexedSearchCTypeMigration)

The following JavaScript modules have been removed:

  • The legacy CKEditor5 alias modules @typo3/ckeditor5-bundle.js and @typo3/ckeditor5-inspector.js have been removed. Use the @ckeditor/ckeditor5-* modules directly. The inspector is available as @ckeditor/ckeditor5-inspector . (Deprecation entry)

The following JavaScript method behaviours have changed:

  • @typo3/backend/form-engine no longer adds the doSave hidden field to the form on save actions. Third-party code must no longer rely on the doSave POST parameter. (Deprecation entry)
  • @typo3/backend/tab no longer dispatches the legacy show.bs.tab and shown.bs.tab events on tab switches. Listen for typo3:tab:show ( TabShowEvent ) and typo3:tab:shown ( TabShownEvent ) instead. (Deprecation entry)

The following JavaScript methods have been removed:

  • createAbstractViewFormElementToolbar() , wireAbstractViewFormElementToolbarEventListeners() , eachTemplateProperty() , renderCheckboxTemplate() , renderSimpleTemplate() , renderSimpleTemplateWithValidators() , renderSelectTemplates() , renderFileUploadTemplates() of @typo3/form/backend/form-editor/stage-component (Deprecation entry)
  • markFieldAsChanged() of @typo3/backend/form-engine-validation . Call markFieldAsChanged() of @typo3/backend/form-engine instead. (Deprecation entry)

The following localization XLIFF files/labels have been removed:

  • Several deprecated files (see commit) have been removed and are too many to list. These can be identified in TYPO3 v14 source files by searching for the XML attribute x-unused-since. (Deprecation entry)

The following template files have been removed:

  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/SimpleTemplate.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/SelectTemplate.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/FileUploadTemplate.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/ContentElement.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/Fieldset.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/StaticText.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/Page.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/SummaryPage.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/_ElementToolbar.fluid.html (Deprecation entry)
  • EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/_UnknownElement.fluid.html (Deprecation entry)

The following CSS classes have been removed:

  • .table-sorting-button and .table-sorting-icon . These backend table sorting helper classes were not used by the core.

The following Fluid rendering mechanisms have been removed:

  • HeaderAssets and FooterAssets Fluid template sections are no longer auto-rendered (Deprecation entry)

The following asset ViewHelper arguments and options have been removed:

  • The useNonce argument of the f:asset.script and f:asset.css ViewHelpers has been removed. Use the csp argument instead. (Deprecation entry)
  • The useNonce option key for JavaScript and stylesheet assets added via \TYPO3\CMS\Core\Page\AssetCollector has been removed. Use the csp option instead. (Deprecation entry)

The following FormEngine result array keys have been removed:

  • additionalHiddenFields , hidden fields are now added to the html key directly (Deprecation entry)

The following cache action array keys have been removed:

  • href in cache actions registered via \TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent ; use endpoint instead (Deprecation entry)

The following features are now always enabled:

  • extbase.consistentDateTimeHandling - Extbase DateTime persistence is aligned with FormEngine and DataHandler, the feature flag has been dropped (Feature introduction)

Impact 

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

Breaking: #109811 - Removed "afterFormStateInitialized" hook 

See forge#109811

Description 

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

In addition, the interface \TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle\AfterFormStateInitializedInterface has been removed as it was only used by the hook.

Impact 

Hook implementations registered under afterFormStateInitialized are no longer executed in TYPO3 v15.0 and later.

Classes implementing \TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle\AfterFormStateInitializedInterface will cause a PHP fatal error.

Affected installations 

TYPO3 installations with custom extensions using this hook or implementing AfterFormStateInitializedInterface are affected.

The extension scanner reports any usage as a strong match.

Migration 

Register a PSR-14 event listener for \TYPO3\CMS\Form\Event\AfterFormStateInitializedEvent instead:

EXT:my_extension/Classes/EventListener/MyAfterFormStateInitializedEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterFormStateInitializedEvent;

#[AsEventListener(identifier: 'my-extension/after-form-state-initialized')]
final readonly class MyAfterFormStateInitializedEventListener
{
    public function __invoke(AfterFormStateInitializedEvent $event): void
    {
        // Access $event->formRuntime->getFormState() here
    }
}
Copied!

Remove the hook registration from ext_localconf.php and the AfterFormStateInitializedInterface implementation from your hook class.

See also the Feature entry.

Breaking: #109849 - Removed "buildFormDefinitionValidationConfiguration" hook 

See forge#109849

Description 

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

Impact 

Any hook implementation registered under this identifier will no longer be executed.

Affected installations 

TYPO3 installations with custom extensions that register a hook class under $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] are affected.

Migration 

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

Use the AfterFormDefinitionValidationConfigurationIsBuiltEvent to achieve the same functionality with the new event-based system.

Breaking: #109926 - Removed extbase parameter type evaluation via DocBlock 

See forge#109926

Description 

Extbase resolved the target type of a controller action argument (and other reflected method parameters) from the method DocBlock when no native PHP type declaration was given:

/**
 * @param \MyVendor\MyExtension\Domain\Model\MyModel $item
 */
public function showAction($item): ResponseInterface
Copied!

This fallback was deprecated with #94115 in TYPO3 v11 in favor of native PHP type declarations. It has now been removed.

As part of this removal, the internal helper class \TYPO3\CMS\Extbase\Reflection\DocBlock\Tags\Null_ , which only existed to silence DocBlock parsing of this argument resolution, has been removed.

Impact 

The type of a method parameter is now solely determined from its native PHP type declaration. A parameter that relies on a @param DocBlock tag without a native type declaration no longer receives a type.

For controller actions this means an \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException is thrown when the action is dispatched. In combination with a #[Validate] attribute, an \TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException is thrown while the class schema is built.

Affected installations 

Extbase extensions with controller actions or other reflected methods that still declare argument types via @param DocBlock tags instead of native PHP type declarations. Such code has emitted a deprecation since TYPO3 v11 and, for the #[Validate] case, already raised a runtime exception since v12.

Migration 

Use native PHP type declarations, available since TYPO3 v10:

public function showAction(\MyVendor\MyExtension\Domain\Model\MyModel $item): ResponseInterface
Copied!

Breaking: #109998 - Removed jQuery 

See forge#109998

Description 

The jQuery JavaScript library is no longer shipped with TYPO3. All remaining usages in the TYPO3 backend have been migrated to native DOM APIs.

In detail, the following pieces have been removed:

  • The bundled library file EXT:core/Resources/Public/JavaScript/Contrib/jquery.js
  • The import map entries jquery and jquery/ provided by EXT:core
  • The JavaScript module @typo3/backend/multi-step-wizard.js together with its global TYPO3.MultiStepWizard object

Note that @typo3/backend/multi-step-wizard.js has been removed without a prior deprecation phase. It was the recommended migration target when @typo3/backend/wizard.js was deprecated with TYPO3 v13.1 (Deprecation entry). The module is unused within TYPO3 itself since v14.2, when the form and translation wizards switched to the internal typo3-backend-wizard web component.

Furthermore, a couple of JavaScript API signatures changed because they returned or received jQuery objects before:

  • FormEngine.getFieldElement() now returns a native HTMLElement or null instead of a jQuery collection
  • The callbacks of the preview, new, duplicate and delete doc header actions of @typo3/backend/form-engine.js (for example FormEngine.showPreviewModal() ) now receive a native HTMLInputElement or HTMLAnchorElement instead of a jQuery object
  • The @typo3/backend/modal.js API no longer accepts jQuery objects as modal content

Impact 

Extensions that import jquery in their backend JavaScript modules will fail to load those modules in the browser because the bare module specifier can no longer be resolved.

Extensions using @typo3/backend/multi-step-wizard.js or the TYPO3.MultiStepWizard global will fail accordingly.

Extensions calling the changed FormEngine and Modal methods with jQuery semantics will trigger JavaScript errors.

Affected installations 

All installations with extensions that load jQuery via the TYPO3 core import map, use the multi step wizard module, or rely on the jQuery based FormEngine and Modal API signatures in their backend JavaScript.

Migration 

jQuery 

Extensions that still depend on jQuery need to ship their own copy of the library and register it in their import map configuration:

EXT:my_extension/Configuration/JavaScriptModules.php
return [
    'dependencies' => ['core'],
    'imports' => [
        'jquery' => 'EXT:my_extension/Resources/Public/JavaScript/Contrib/jquery.js',
    ],
];
Copied!

Consider migrating extension JavaScript to native DOM APIs instead.

Multi step wizard 

There is no direct replacement for the removed @typo3/backend/multi-step-wizard.js module. TYPO3 itself uses the typo3-backend-wizard web component, which is currently internal and provides no API stability guarantees yet. Extensions should either bring their own wizard implementation, or copy the removed module into their own codebase together with a bundled jQuery.

FormEngine 

Adapt callers of the changed FormEngine methods to the native return types:

-const field = FormEngine.getFieldElement(fieldName).get(0);
+const field = FormEngine.getFieldElement(fieldName);
Copied!

Breaking: #110023 - Harden type declarations 

See forge#110023

Description 

The following PHP methods now use strict type declarations instead of loose type hints (PHPdoc annotations).

This is considered a breaking change in case consumers are not adjusted for strict types.

  • \TYPO3\CMS\Backend\Utility::daysUntil - Parameter $tstamp can now only be of type integer or DateTimeInterface - cast to (int) if strings were passed to this argument before.
  • \TYPO3\CMS\Backend\Form\FormDataGroupInterface::compile - Return type now specifies array, implementations of this interface need to declare this as well.

Impact 

Using the mentioned methods with wrong types will now result in a PHP exception, fatal error or warning, depending on configured error reporting.

Migration 

Ensure proper PHP type declarations are used when calling these methods.

Breaking: #110027 - StandardContentPreviewRenderer streamlined 

See forge#110027

Description 

\TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer has been turned into a stateless, dependency-injected service. It is now declared final and can no longer be subclassed. The lazy initialize() workaround that resolved its dependencies through \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() on first use has been removed, and all dependencies are passed through the constructor instead.

Impact 

Custom preview renderers extending \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer raise a fatal error, as the class is now final .

Affected installations 

Instances with third-party extensions that subclass StandardContentPreviewRenderer or instantiate it manually.

Migration 

Instead of subclassing StandardContentPreviewRenderer , implement \TYPO3\CMS\Backend\Preview\PreviewRendererInterface directly. The standard renderer may be composed and its rendering methods delegated to where its output is desired, as \TYPO3\CMS\Form\Preview\FormPagePreviewRenderer demonstrates.

Breaking: #110028 - LoggerAwareInterface removed from FormEngine base classes 

See forge#110028

Description 

The FormEngine base classes \TYPO3\CMS\Backend\Form\AbstractNode and \TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider no longer implement \Psr\Log\LoggerAwareInterface and no longer use \Psr\Log\LoggerAwareTrait .

Both base classes never read the injected logger themselves. The node hierarchy built on AbstractNode does not use a logger at all. The two data provider subclasses that did log, \TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles and \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline , now receive a \Psr\Log\LoggerInterface through their constructor instead.

Impact 

Subclasses of these base classes, for example custom FormEngine nodes or custom database record providers shipped by extensions, are no longer recognized as \Psr\Log\LoggerAwareInterface instances and no longer have a logger injected automatically. The inherited setLogger() method is gone, and reading the previously inherited $logger property raises an error.

Affected installations 

Instances with third-party extensions that subclass \TYPO3\CMS\Backend\Form\AbstractNode or \TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider and rely on the automatically injected logger.

Migration 

Request a \Psr\Log\LoggerInterface as a constructor argument in the affected subclass. The subclass must be registered as a public service using the #[Autoconfigure(public: true)] attribute, so that the FormEngine \TYPO3\CMS\Backend\Form\NodeFactory obtains it through the dependency injection container, which then resolves the constructor argument automatically.

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Form\AbstractNode;

#[Autoconfigure(public: true)]
final class MyFormElement extends AbstractNode
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function render(): array
    {
        $this->logger->warning('Something the element wants to log');
        // ...
    }
}
Copied!

Breaking: #110188 - DataHandler: Drop property bypassAccessCheckForRecords 

See forge#110188

Description 

The property DataHandler->bypassAccessCheckForRecords has been removed.

Impact 

Using DataHandler->bypassAccessCheckForRecords will raise a PHP warning or a fatal error. There will be no effect on access checks anymore.

Affected installations 

All installations using DataHandler->bypassAccessCheckForRecords . This could be in DataHandler hooks or in custom DataHandler calls.

Migration 

Use a proper BackendUserAuthentication with sufficient access rights in DataHandler->start() instead for custom DataHandler calls. Check for DataHandler->BE_USER rights in DataHandler hooks.

Breaking: #110196 - PHP class Rfc822AddressesParser removed 

See forge#110196

Description 

The PHP class \TYPO3\CMS\Core\Mail\Rfc822AddressesParser has been removed. The class was a modernized copy of the PEAR package pear/mail , dating back to 2010, and was only used internally by \TYPO3\CMS\Core\Utility\MailUtility::parseAddresses() .

This internal usage is now based on \Symfony\Component\Mime\Address of the symfony/mime package, which TYPO3 already utilizes for sending emails.

Impact 

Instantiating or referencing the class Rfc822AddressesParser will raise a fatal PHP error.

In addition, MailUtility::parseAddresses() now strips surrounding double quotes from display names: parsing '"last, first" <email@example.org>' previously returned the display name '"last, first"' and now returns 'last, first' . Quoting is re-applied automatically by symfony/mime when composing a mail message.

Affected installations 

TYPO3 installations with third-party extensions directly using the class Rfc822AddressesParser . The extension scanner reports any usage as a strong match.

Migration 

To parse a comma-separated list of email addresses with optional display names, use \TYPO3\CMS\Core\Utility\MailUtility::parseAddresses() , which continues to work as before.

Alternatively, use the symfony/mime API directly to parse a single mailbox string:

use Symfony\Component\Mime\Address;

$address = Address::create('John Doe <john.doe@example.org>');
$email = $address->getAddress();
$name = $address->getName();
Copied!

Breaking: #110211 - Raise minimum PHP version to 8.5 

See forge#110211

Description 

The minimum PHP version required to run TYPO3 has been raised to PHP 8.5.

Impact 

The TYPO3 Core codebase and extensions tailored for this version can use features implemented with PHP up to and including 8.5. Running TYPO3 with older PHP versions will trigger fatal errors.

Affected installations 

All installations running a PHP version lower than 8.5.

Migration 

Update the PHP platform to PHP 8.5 before upgrading to this TYPO3 version. Previous TYPO3 versions already support PHP 8.5, which allows upgrading the PHP platform in a first step and TYPO3 in a second step.

Breaking: #110218 - Class LogRecord declared final 

See forge#110218

Description 

The PHP class \TYPO3\CMS\Core\Log\LogRecord — the value object passed to log writers and processors — has been declared final and is now instantiated directly via new within \TYPO3\CMS\Core\Log\Logger , instead of using GeneralUtility::makeInstance() .

A log record is a plain data transfer object created for every single log entry. Routing its creation through makeInstance() allowed the class to be overridden via XCLASS, but caused an unnecessary container lookup in one of the most frequently executed code paths of the logging API.

In addition, the class now declares strict_types and uses native type declarations for all method signatures instead of loose PHPDoc annotations.

Impact 

Extending or XCLASSing LogRecord is not possible anymore. Extension classes will raise a fatal PHP error, XCLASS configurations for this class are silently ignored.

Calling a method of LogRecord with a wrong argument type — for example a non-float value for setCreated() — will raise a PHP \TypeError , depending on the strict_types mode of the calling code.

Creating, reading and modifying log records — for example in custom implementations of \TYPO3\CMS\Core\Log\Processor\ProcessorInterface or \TYPO3\CMS\Core\Log\Writer\WriterInterface — continues to work as before.

Affected installations 

TYPO3 installations with third-party extensions that extend or XCLASS the class LogRecord , which is very unlikely.

Migration 

To enrich or modify log records, implement a custom log processor ( ProcessorInterface ), which may return a modified or newly created LogRecord instance. Custom output handling belongs into a log writer ( WriterInterface ).

Breaking: #110219 - Log request ID provided by log processor 

See forge#110219

Description 

The unique ID of the current request, used to correlate all log entries written during a single request, was previously passed as constructor argument from \TYPO3\CMS\Core\Log\LogManager to every \TYPO3\CMS\Core\Log\Logger instance, which in turn copied it into each created \TYPO3\CMS\Core\Log\LogRecord .

The request ID is now added to log records at logging time by the new log processor \TYPO3\CMS\Core\Log\Processor\RequestIdProcessor , which LogManager attaches automatically to every logger it creates, for all severity levels covered by the configured log writers. The processor is registered internally on purpose — not via $GLOBALS['TYPO3_CONF_VARS']['LOG'] — so it cannot be removed accidentally by overriding the global processor configuration.

The following method signatures have changed:

  • Logger::__construct() no longer accepts a second $requestId argument, the protected property Logger::$requestId has been removed
  • LogManager::__construct() now expects a \TYPO3\CMS\Core\Core\RequestId object instead of a string, and creates one itself if omitted
  • The protected method LogManager::makeLogger() no longer receives a $requestId argument

The generated log output is unchanged: log records written by configured writers carry the same request ID as before, available via LogRecord::getRequestId() .

Impact 

Instantiating LogManager with a string request ID will raise a PHP \TypeError .

Passing a second argument to the Logger constructor is ignored. Log records created by a manually instantiated Logger — bypassing LogManager — no longer contain a request ID, unless the RequestIdProcessor is attached manually.

Affected installations 

TYPO3 installations with third-party extensions instantiating Logger or LogManager directly with a custom request ID, which is very unlikely. Extensions obtaining loggers via dependency injection, the #[Channel] attribute, LoggerAwareInterface or LogManager->getLogger() are not affected.

Migration 

Obtain loggers through dependency injection or LogManager->getLogger() , which attach the RequestIdProcessor automatically.

For manually created loggers that should add the request ID to their log records, attach the processor explicitly:

use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Log\Processor\RequestIdProcessor;

$logger = new Logger('my.channel');
$logger->addWriter(LogLevel::WARNING, $myWriter);
$logger->addProcessor(LogLevel::WARNING, new RequestIdProcessor(new RequestId()));
Copied!

Breaking: #110221 - Class Logger declared final 

See forge#110221

Description 

The PHP class \TYPO3\CMS\Core\Log\Logger has been declared final and is now instantiated directly via new within \TYPO3\CMS\Core\Log\LogManager , instead of using GeneralUtility::makeInstance() .

The logger is a plain, per-channel object created and configured by the LogManager . Routing its creation through makeInstance() only served to allow overriding the class via XCLASS, which is not a supported extension point for the logging API.

Impact 

Extending or XCLASSing Logger is not possible anymore. Extension classes will raise a fatal PHP error, XCLASS configurations for this class are silently ignored.

Obtaining and using loggers via dependency injection, the #[Channel] attribute, LoggerAwareInterface or LogManager->getLogger() continues to work as before.

Affected installations 

TYPO3 installations with third-party extensions that extend or XCLASS the class Logger , which is very unlikely.

Migration 

To customize logging behavior, implement the PSR-3 \Psr\Log\LoggerInterface and provide the instances through a custom \TYPO3\CMS\Core\Log\LogManagerInterface implementation, or attach custom log writers ( \TYPO3\CMS\Core\Log\Writer\WriterInterface ) and processors ( \TYPO3\CMS\Core\Log\Processor\ProcessorInterface ) to the existing logger, which is the designated way to influence how log records are handled.

Breaking: #110250 - Remove implicit output buffering from Bootstrap 

See forge#110250

Description 

The class \TYPO3\CMS\Core\Core\Bootstrap no longer wraps the entire bootstrap and request lifecycle in an implicit output buffer via ob_start() . This buffer was historically opened as the very first action in Bootstrap::init() to swallow accidental stray output (warnings, notices or stray echo / print statements, typically from ext_localconf.php / ext_tables.php of third-party extensions) so it would not corrupt AJAX responses or file downloads. On CLI this buffering was already disabled again in a previous change, as it delayed console output until the process ended and could bloat memory for commands producing a lot of output.

Since responses are fully assembled as PSR-7 message bodies before being emitted, this legacy safety net is not needed anymore.

Along with it, the now unused method Bootstrap::startOutputBuffering() has been removed.

Impact 

Any stray output produced during bootstrap (for example from ext_localconf.php ) is now sent to the client directly instead of being silently discarded later on. This mainly affects binary/file responses and AJAX requests, which relied on the buffer being cleaned right before the response body was emitted.

Calling Bootstrap::startOutputBuffering() now results in a fatal PHP error ( Call to undefined method ).

Affected installations 

TYPO3 installations with extensions that produce accidental output during ext_localconf.php / ext_tables.php loading, which so far was silently swallowed, and installations with custom entry point scripts calling Bootstrap::startOutputBuffering() directly.

Migration 

Remove any accidental output from ext_localconf.php and ext_tables.php files. Extensions that need to suppress their own output must manage output buffering ( ob_start() / ob_end_clean() ) themselves.

Remove calls to Bootstrap::startOutputBuffering() and use ob_start() directly, if the behaviour is still required.

Breaking: #110277 - File renderer registration and interface changed 

See forge#110277

Description 

File renderers, used for example by the <f:media> ViewHelper to render audio, video or online media files, are now registered as tagged services via dependency injection (see Feature: #110277 - Register file renderers as tagged services). This comes with the following breaking changes:

  • \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry->registerRendererClass() is now a no-op. Calling the method has no effect anymore, but triggers an E_USER_DEPRECATED notice. The method will be removed in TYPO3 v16.0.
  • \TYPO3\CMS\Core\Resource\Rendering\FileRendererInterface does not extend \TYPO3\CMS\Core\SingletonInterface anymore. File renderers are shared services managed by the dependency injection container.
  • The method getPriority() has been removed from FileRendererInterface . The renderer priority is now defined at registration time via the #[AsFileRenderer] attribute or the fal.file_renderer service tag. Note that the file renderers shipped with TYPO3 Core previously returned a priority of 1 from getPriority() and are now registered with the attribute's default priority of 0 . Custom renderers that relied on a priority of 0 to rank strictly below all Core renderers now rank equally with them instead and should use a negative priority to keep the previous ordering.
  • Renderers registered with the same priority are no longer guaranteed to be asked in the order they were added: previously, same-priority renderers kept the order in which RendererRegistry->registerRendererClass() was called from ext_localconf.php. The order of same-priority tagged services is now an implementation detail of the dependency injection container and must not be relied upon. Extensions that depend on a specific evaluation order between renderers should assign distinct priorities instead.
  • The remaining methods of FileRendererInterface are now strictly typed: canRender(FileInterface $file): bool and render(FileInterface $file, int|string $width, int|string $height, array $options = []): string .
  • The methods createRendererInstance() and compareRendererPriority() have been removed from RendererRegistry , the method getRendererInstances() has been changed from public to protected visibility, and the remaining methods are now strictly typed.
  • RendererRegistry is now marked as @internal , since registering file renderers does not require interacting with the registry anymore. TYPO3 Core resolves the matching renderer via getRenderer() internally, for example in the <f:media> ViewHelper.

Impact 

File renderers registered via RendererRegistry->registerRendererClass() in ext_localconf.php are no longer evaluated. The corresponding files (audio, video or online media) are no longer rendered by the custom renderer until it is registered as a tagged service.

Custom renderer classes implementing FileRendererInterface without the adapted method signatures will cause a fatal PHP error.

Affected installations 

All installations with custom extensions registering file renderers via RendererRegistry->registerRendererClass() , or providing custom implementations of FileRendererInterface . The extension scanner reports usages of registerRendererClass() as weak match.

Migration 

Remove the RendererRegistry->registerRendererClass() call from ext_localconf.php and add the #[AsFileRenderer] attribute to the renderer class instead. Move the priority previously returned by getPriority() to the attribute and remove the method. Add the native type declarations to canRender() and render() :

EXT:my_extension/Classes/Resource/Rendering/MyVideoRenderer.php
use TYPO3\CMS\Core\Attribute\AsFileRenderer;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Rendering\FileRendererInterface;

#[AsFileRenderer(priority: 10)]
final class MyVideoRenderer implements FileRendererInterface
{
    public function canRender(FileInterface $file): bool
    {
        // ...
    }

    public function render(FileInterface $file, int|string $width, int|string $height, array $options = []): string
    {
        // ...
    }
}
Copied!

In case the extension supports both TYPO3 v14 and v15, keep the getPriority() method (it is simply unused in v15) and register the renderer in both ways: the ext_localconf.php registration is evaluated in v14, the attribute in v15. Since PHP parameter types must not be narrowed in implementations, keep the $width and $height parameters untyped in this case — only the bool and string return type declarations are compatible with both versions.

Code that called RendererRegistry->getRendererInstances() to inspect all registered renderers should inject RendererRegistry and use getRenderer($file) to retrieve the matching renderer for a given file instead.

Breaking: #110286 - Text extractor registration and interface changed 

See forge#110286

Description 

Text extractors are now registered as tagged services via dependency injection (see Feature: #110286 - Register text extractors as tagged services). This comes with the following breaking changes:

  • \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorRegistry->registerTextExtractor() is now a no-op. Calling the method has no effect anymore. The method will be removed in TYPO3 v16.0.
  • Extractors registered with the same priority are no longer guaranteed to be asked in the order they were added: previously, extractors kept the order in which TextExtractorRegistry->registerTextExtractor() was called from ext_localconf.php. The order of same-priority tagged services is now an implementation detail of the dependency injection container and must not be relied upon. Extensions that depend on a specific evaluation order between extractors should assign distinct priorities instead.
  • The methods of \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorInterface are now strictly typed: canExtractText(FileInterface $file): bool and extractText(FileInterface $file): string .
  • The method createTextExtractorInstance() has been removed from TextExtractorRegistry , the method getTextExtractorInstances() has been changed from public to protected visibility, and the remaining methods are now strictly typed. The public API of the registry is getTextExtractor() , which returns the first matching extractor for a given file.

Impact 

Text extractors registered via TextExtractorRegistry->registerTextExtractor() in ext_localconf.php are no longer evaluated: no text is extracted by the custom extractor until it is registered as a tagged service.

Custom extractor classes implementing TextExtractorInterface without the adapted method signatures will cause a fatal PHP error.

Affected installations 

All installations with custom extensions registering text extractors via TextExtractorRegistry->registerTextExtractor() , or providing custom implementations of TextExtractorInterface . The extension scanner reports usages of registerTextExtractor() as weak match.

Migration 

Remove the TextExtractorRegistry->registerTextExtractor() call from ext_localconf.php and add the #[AsTextExtractor] attribute to the extractor class instead. Add the native type declarations to canExtractText() and extractText() :

EXT:my_extension/Classes/Resource/TextExtraction/PdfTextExtractor.php
use TYPO3\CMS\Core\Attribute\AsTextExtractor;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorInterface;

#[AsTextExtractor(priority: 10)]
final class PdfTextExtractor implements TextExtractorInterface
{
    public function canExtractText(FileInterface $file): bool
    {
        // ...
    }

    public function extractText(FileInterface $file): string
    {
        // ...
    }
}
Copied!

In case the extension supports both TYPO3 v14 and v15, register the extractor in both ways: the ext_localconf.php registration is evaluated in v14, the attribute in v15. The bool and string return type declarations are compatible with both versions.

Code that called TextExtractorRegistry->getTextExtractorInstances() to inspect all registered extractors should inject TextExtractorRegistry and use getTextExtractor($file) to retrieve the matching extractor for a given file instead.

Breaking: #110287 - Meta tag manager registration and interface changed 

See forge#110287

Description 

Meta tag managers are now registered as tagged services via dependency injection (see Feature: #110287 - Register meta tag managers as tagged services). This comes with the following breaking changes:

  • \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry->registerManager() is now a no-op. Calling the method has no effect anymore. The method will be removed in TYPO3 v16.0.
  • The method removeAllManagers() has been removed from MetaTagManagerRegistry , since the list of managers is compiled into the dependency injection container and cannot be changed at runtime anymore.
  • The methods addProperty() , removeProperty() and removeAllProperties() of \TYPO3\CMS\Core\MetaTag\MetaTagManagerInterface now declare a native void return type.

Impact 

Meta tag managers registered via MetaTagManagerRegistry->registerManager() in ext_localconf.php are no longer evaluated: their meta tag properties are handled by the generic meta tag manager until the manager is registered as a tagged service.

Custom classes implementing MetaTagManagerInterface directly without the adapted method signatures will cause a fatal PHP error. Classes extending \TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager are only affected if they override one of the changed methods.

Affected installations 

All installations with custom extensions registering meta tag managers via MetaTagManagerRegistry->registerManager() , or providing custom implementations of MetaTagManagerInterface . The extension scanner reports usages of the changed methods as weak match.

Migration 

Remove the MetaTagManagerRegistry->registerManager() call from ext_localconf.php and add the #[AsMetaTagManager] attribute to the manager class instead:

EXT:my_extension/Classes/MetaTag/MyMetaTagManager.php
use TYPO3\CMS\Core\Attribute\AsMetaTagManager;
use TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager;

#[AsMetaTagManager(identifier: 'my-manager')]
final class MyMetaTagManager extends AbstractMetaTagManager
{
    // ...
}
Copied!

Move the manager name previously passed as first argument to registerManager() to the identifier argument of the attribute, and any before / after arguments to the corresponding attribute arguments.

In case the extension supports both TYPO3 v14 and v15, register the manager in both ways: the ext_localconf.php registration is evaluated in v14, the attribute in v15. The void return type declarations are compatible with both versions.

Breaking: #110319 - Encryption key is checked before ext_localconf.php loading 

See forge#110319

Description 

\TYPO3\CMS\Core\Core\Bootstrap verifies that $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] is not empty and throws a \RuntimeException otherwise. This check has been moved to run before the ext_localconf.php files of all active extensions are loaded. Previously it ran afterwards, and additionally after $GLOBALS['TCA'] had been populated.

The encryption key is part of the system configuration and is used for security-relevant functionality. It must therefore be in place before any extension code is executed during bootstrap.

In addition, $GLOBALS['TCA'] is now assigned after TcaSchemaFactory->load() has been called, so consumers can not observe a state where the global array is populated but the schema information is not yet available.

Impact 

Installations that set $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] from within an ext_localconf.php file now run into the "TYPO3 Encryption Key is empty" exception, as the check is executed before the file is evaluated.

Code within ext_localconf.php files can not rely on $GLOBALS['TCA'] being available anymore - which was never a guaranteed state, as ext_localconf.php files are loaded before TCA is built. This is unchanged, but the assignment now happens later within the bootstrap.

Affected installations 

Installations that populate the encryption key at runtime from an ext_localconf.php file, for instance by reading it from an environment variable or an external secret store.

Migration 

Set the encryption key in the system configuration, either directly in config/system/settings.php or in config/system/additional.php, both of which are evaluated before the check:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = getenv('TYPO3_ENCRYPTION_KEY');
Copied!

Breaking: #110319 - Removed unused internal Bootstrap methods 

See forge#110319

Description 

The following methods of \TYPO3\CMS\Core\Core\Bootstrap have been removed:

  • Bootstrap::baseSetup()
  • Bootstrap::createConfigurationManager()
  • Bootstrap::populateLocalConfiguration()

All three were marked @internal and are not used by TYPO3 Core anymore. Bootstrap::baseSetup() only registered the class loading information in non-Composer mode, which Bootstrap::init() does itself. The two configuration related methods have been inlined into Bootstrap::init() , as they consisted of a single statement each.

Impact 

Calling one of the removed methods results in a fatal PHP error ( Call to undefined method ).

Affected installations 

TYPO3 installations with third-party extensions or custom entry point scripts that call Bootstrap::baseSetup() or Bootstrap::createConfigurationManager() directly - a pattern sometimes used in custom, non-Composer bootstrapping scripts. Bootstrap::populateLocalConfiguration() was already protected and could not be called from the outside.

The extension scanner reports possible usages.

Migration 

Remove calls to Bootstrap::baseSetup() , Bootstrap::init() performs the equivalent setup internally.

Replace Bootstrap::createConfigurationManager() with a direct instantiation:

$configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager();
Copied!

Breaking: #110334 - XML sitemap data provider interface changed 

See forge#110334

Description 

\TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface declared a constructor, which forced every data provider to be instantiated with its runtime information as constructor arguments. Data providers could therefore never be resolved by the dependency injection container and had to fetch all their dependencies statically. In addition, the items of a sitemap had to be collected in the constructor, since the interface methods took no arguments.

The interface has been changed accordingly:

  • The constructor declaration and the methods getKey() , getItems() , getLastModified() and getNumberOfPages() have been removed in favor of the single method getSitemap() .
  • getSitemap() receives all runtime information as \TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRequest : The name of the requested sitemap, its configuration, the number of the requested page, the current request and a content object renderer to generate URLs with.
  • getSitemap() returns a \TYPO3\CMS\Seo\XmlSitemap\XmlSitemap carrying the items of the requested page, the last modification date and the number of pages of the sitemap.
  • Data providers are stateless services now. The interface is tagged with seo.xmlsitemap.provider , so implementations are registered in the dependency injection container automatically and are free to use constructor injection.

Registering a sitemap in TypoScript is unchanged: Data providers are still referenced by their class name.

Impact 

Classes implementing XmlSitemapDataProviderInterface without the method getSitemap() will cause a fatal PHP error.

Data providers that are not available as a service in the dependency injection container are not resolved anymore: Rendering such a sitemap throws a \TYPO3\CMS\Seo\XmlSitemap\Exception\InvalidConfigurationException , and the sitemap is skipped in the sitemap index.

Data providers extending \TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider keep working unchanged, see Deprecation: #110334 - AbstractXmlSitemapDataProvider.

Affected installations 

All installations with custom extensions implementing XmlSitemapDataProviderInterface directly. This is a rarely used API, most data providers extend AbstractXmlSitemapDataProvider instead.

Migration 

Move the collecting of items from the constructor to getSitemap() and take the configuration, the requested page and the content object renderer from the XmlSitemapRequest . The constructor is free for dependency injection afterwards:

EXT:my_extension/Classes/XmlSitemap/MyXmlSitemapDataProvider.php
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemap;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRequest;

final readonly class MyXmlSitemapDataProvider implements XmlSitemapDataProviderInterface
{
    public function __construct(
        private MyItemRepository $itemRepository,
    ) {}

    public function getSitemap(XmlSitemapRequest $sitemapRequest): XmlSitemap
    {
        $items = [];
        foreach ($this->itemRepository->findAll($sitemapRequest->configuration) as $item) {
            $items[] = [
                'uid' => $item['uid'],
                'lastMod' => $item['tstamp'],
            ];
        }
        return XmlSitemap::forPage(
            $items,
            $sitemapRequest->page,
            fn(array $item): array => $this->defineUrl($item, $sitemapRequest),
        );
    }

    private function defineUrl(array $item, XmlSitemapRequest $sitemapRequest): array
    {
        $item['loc'] = $sitemapRequest->contentObjectRenderer->createUrl([
            'parameter' => $item['uid'],
            'forceAbsoluteUrl' => 1,
        ]);
        return $item;
    }
}
Copied!

XmlSitemap::forPage() takes all items of a sitemap, extracts the items of the requested page and calculates the last modification date and the number of pages needed by the sitemap index. The optional item mapper is applied to the items of the requested page only, and only when the items are rendered at all - the sitemap index does not generate any URL this way. Data providers taking care of paging themselves, for example by limiting their database query to the items of the requested page, use XmlSitemap::create() instead:

return XmlSitemap::create(
    fn(): array => $this->itemRepository->findForPage($sitemapRequest->page),
    $this->itemRepository->findLastModified(),
    $this->itemRepository->countPages(),
);
Copied!

In case the extension supports both TYPO3 v14 and v15, extend AbstractXmlSitemapDataProvider instead of implementing the interface directly: Such a data provider is instantiated with its runtime information in v15 as well, at the cost of a deprecation message. It is then migrated to the new interface once support for TYPO3 v14 is dropped.

Breaking: #110414 - Remove global access time variables 

See forge#110414

Description 

The global variables $GLOBALS['ACCESS_TIME'] and $GLOBALS['SIM_ACCESS_TIME'] have been removed.

Both were introduced in 2008 and held the current (respectively the simulated) execution time floored to the full minute. Their only purpose was to keep the SQL of starttime / endtime comparisons textually identical for up to 60 seconds, so that the MySQL query cache could serve repeated requests. That query cache has been removed from MySQL and is disabled by default in MariaDB, so the original motivation no longer applies.

Beyond that, both values were pure derivatives of $GLOBALS['EXEC_TIME'] and $GLOBALS['SIM_EXEC_TIME'] that had to be kept in sync manually, which was an error-prone contract.

The minute granularity itself is kept, since record visibility and the frontend cache lifetime derived from starttime / endtime must use the very same clock. It now lives in the date aspect of the Context API, which gained two typed methods:

  • \TYPO3\CMS\Core\Context\DateTimeAspect::getTimestamp()
  • \TYPO3\CMS\Core\Context\DateTimeAspect::getTimestampWithMinutePrecision()

The already existing property $context->getPropertyFromAspect('date', 'accessTime') is unchanged and now delegates to the latter method.

As a consequence, \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction and \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction fall back to the date aspect instead of the global variable when no timestamp is handed in. The \RuntimeException with codes 1462820645 and 1462821084 , which was thrown when the global was missing, has become unreachable and has been removed.

Impact 

Reading $GLOBALS['ACCESS_TIME'] or $GLOBALS['SIM_ACCESS_TIME'] raises an "Undefined array key" warning and evaluates to null .

Writing to either variable no longer has any effect. In particular, simulating another point in time by setting $GLOBALS['SIM_ACCESS_TIME'] is silently ignored, and records are then evaluated against the real current time.

The following constructors received an additional \TYPO3\CMS\Core\Context\Context argument:

  • \TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator
  • \TYPO3\CMS\Redirects\Service\RedirectService
  • \TYPO3\CMS\Backend\Controller\PageLayoutController

Affected installations 

Installations with extensions that read or write one of the two global variables, that rely on the removed exceptions of the start and end time restrictions, or that instantiate one of the above classes manually instead of using dependency injection.

Migration 

Use the date aspect of the Context API instead of the global variables:

use TYPO3\CMS\Core\Context\Context;

// Before
$accessTime = $GLOBALS['SIM_ACCESS_TIME'];

// After, with dependency injection
public function __construct(private readonly Context $context) {}

$accessTime = $this->context->getAspect('date')->getTimestampWithMinutePrecision();
Copied!

In places without dependency injection, the current context is fetched via GeneralUtility::makeInstance(Context::class) .

To simulate another point in time, set the date aspect instead of writing to $GLOBALS['SIM_ACCESS_TIME'] :

use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Domain\DateTimeFactory;

$context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($timestamp)));
Copied!

Breaking: #110485 - Menu content object registration changed 

See forge#110485

Description 

Menu content objects - the sub types of the HMENU content object, for example TMENU - are now registered as tagged services via dependency injection. This comes with the following breaking changes:

  • The class \TYPO3\CMS\Frontend\ContentObject\Menu\MenuContentObjectFactory has been removed. It was marked @internal , but its method registerMenuType() was the only way to add or override a menu type and was therefore used from ext_localconf.php.
  • The exception \TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException has been removed. An unregistered menu type is no longer signalled by an exception, the menu is simply not rendered - which is the behavior that was already visible from the outside, since all Core call sites caught the exception and continued silently.
  • \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject now declares a constructor which requires the service locator of all registered menu content objects as first argument. Subclasses which declare their own constructor must hand this argument to parent::__construct() .
  • Menu content objects are never shared: every HMENU level is rendered by its own instance. Instantiating them through GeneralUtility::makeInstance() is not supported anymore, they have to be retrieved from the service locator.

The constructor argument of AbstractMenuContentObject is set by the dependency injection container for all services tagged as frontend.menucontentobject , together with shared: false . Registering a menu type therefore only requires the tag itself.

Impact 

Menu types registered via MenuContentObjectFactory->registerMenuType() in ext_localconf.php cause a fatal PHP error, since the class does not exist anymore.

Custom classes extending AbstractMenuContentObject with an own constructor cause a fatal PHP error as soon as they are instantiated, unless they pass the service locator on to parent::__construct() .

Affected installations 

All installations with custom extensions registering an own menu type, or overriding the TMENU implementation. Since the only menu type shipped by TYPO3 Core is TMENU , most installations are not affected. The extension scanner reports usages of MenuContentObjectFactory and registerMenuType() .

Migration 

Remove the registerMenuType() call from ext_localconf.php and register the menu content object as a tagged service instead. The identifier is the menu type as used in TypoScript and has to be written in upper case:

EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  My\Extension\ContentObject\Menu\FancyMenuContentObject:
    tags:
      - name: frontend.menucontentobject
        identifier: 'FANCYMENU'
Copied!

The menu type can then be used in TypoScript as before:

page.10 = HMENU
page.10 {
    1 = FANCYMENU
    1 {
        NO = 1
    }
}
Copied!

An existing menu type - including TMENU - is overridden by registering an own class with the same identifier . As extensions are loaded after TYPO3 Core, the last registration for an identifier wins.

Custom classes extending AbstractMenuContentObject which declare their own constructor have to accept and pass on the service locator:

EXT:my_extension/Classes/ContentObject/Menu/FancyMenuContentObject.php
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject;

final class FancyMenuContentObject extends AbstractMenuContentObject
{
    public function __construct(
        ContainerInterface $menuContentObjectLocator,
        private readonly MyOwnDependency $myOwnDependency,
    ) {
        parent::__construct($menuContentObjectLocator);
    }
}
Copied!

Code which created a menu content object through MenuContentObjectFactory->getMenuObjectByType() should inject the locator of all registered menu content objects instead:

EXT:my_extension/Configuration/Services.yaml
services:
  My\Extension\DataProcessing\MyMenuProcessor:
    shared: false
    arguments:
      $menuContentObjectLocator: !tagged_locator { tag: 'frontend.menucontentobject', index_by: 'identifier' }
Copied!
use Psr\Container\ContainerInterface;

final class MyMenuProcessor
{
    public function __construct(
        private readonly ContainerInterface $menuContentObjectLocator,
    ) {}

    public function process(): void
    {
        $menu = $this->menuContentObjectLocator->get('TMENU');
        // ...
    }
}
Copied!

Breaking: #110578 - SingletonInterface removed from core services 

See forge#110578

Description 

\TYPO3\CMS\Core\SingletonInterface predates dependency injection. Today it has exactly one effect: it turns a class into a shared and public service in the dependency injection container, so that the legacy GeneralUtility::makeInstance() can still reach it.

Both properties are already the default for injected services. Every service in the container is shared, so every class that receives a service through constructor injection gets the very same instance - that is what the marker used to guarantee. A service only needs to be public on top of that if it is pulled out of the container by hand.

The marker has therefore been removed from a number of Core services that are resolved through dependency injection anyway. Their lifetime does not change: they are still shared.

Impact 

These classes do not implement SingletonInterface anymore:

  • \TYPO3\CMS\Adminpanel\Service\ConfigurationService
  • \TYPO3\CMS\Adminpanel\Service\ProcessedImageCollector
  • \TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection
  • \TYPO3\CMS\Core\Cache\CacheManager
  • \TYPO3\CMS\Core\Console\CommandRegistry
  • \TYPO3\CMS\Core\Error\AbstractExceptionHandler
  • \TYPO3\CMS\Core\Localization\Locales
  • \TYPO3\CMS\Core\Messaging\FlashMessageService
  • \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry
  • \TYPO3\CMS\Core\PageTitle\PageTitleProviderManager
  • \TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry
  • \TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry
  • \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry
  • \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorRegistry
  • \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
  • \TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter
  • \TYPO3\CMS\Extbase\Service\ExtensionService
  • \TYPO3\CMS\Extbase\Validation\ValidatorResolver
  • \TYPO3\CMS\Install\Service\SessionService
  • \TYPO3\CMS\Scheduler\Scheduler
  • \TYPO3\CMS\Workspaces\Service\Dependency\CollectionService

GeneralUtility::setSingletonInstance() and GeneralUtility::removeSingletonInstance() only accept SingletonInterface instances and raise a TypeError for these classes. This mostly affects tests that substitute one of them.

Every type converter extending AbstractTypeConverter is affected as well, since the marker was inherited. Converters registered with the extbase.type_converter tag stay public, because the tag makes them public on its own.

Some of these services are not public anymore either, so fetching them with GeneralUtility::makeInstance() or $container->get() fails with an ArgumentCountError or a ServiceNotFoundException . Whether a Core service is public is an implementation detail and must not be relied upon.

Affected installations 

Installations with extensions that fetch one of the listed classes through GeneralUtility::makeInstance() or the container, or that replace one of them in tests via GeneralUtility::setSingletonInstance() .

Migration 

Inject the service instead of fetching it. Since services are shared, the injected instance is the same one the rest of the request uses:

EXT:my_extension/Classes/Service/MyService.php
use TYPO3\CMS\Core\Localization\Locales;

final readonly class MyService
{
    public function __construct(
        private Locales $locales,
    ) {}

    public function doSomething(): void
    {
        // instead of GeneralUtility::makeInstance(Locales::class)
        $this->locales->createLocale('de');
    }
}
Copied!

The same applies to tests that replaced one of these classes globally with GeneralUtility::setSingletonInstance() . Hand the dependency to the subject instead: a stub or a mock if the test only needs the collaborator to be there, or a functional test if it should exercise the real one.

This is cleaner and framework-agnostic: the subject is built with plain PHP, and everything it works with is visible in the test itself, instead of being smuggled in through a global registry that the code under test happens to read from.

SingletonInterface itself is not deprecated and keeps working for custom classes. If a class of your own cannot use dependency injection - typically because it is instantiated with constructor arguments, which bypasses the container - it can also be made public explicitly, without the marker:

EXT:my_extension/Classes/Service/MyLegacyService.php
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true)]
final class MyLegacyService
{
}
Copied!

Feature: #94822 - Multiple records with record browser in forms 

See forge#94822

Description 

The record browser in forms can now be used to add multiple records to a property, if configured accordingly.

When the option maxItems of the Inspector-Typo3WinBrowserEditor is greater than 1, the UIDs of the selected records are added as a comma-separated list:

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            MyCustomElement:
              formEditor:
                editors:
                  # ...
                  300:
                    identifier: myRecords
                    # ...
                    minItems: 1
                    maxItems: 10
                    propertyPath: properties.myRecordUids
                    # ...
                    propertyValidators:
                      10: IntegerList
                      # ...
Copied!

Custom logic must be added to actually make use of the multiple values.

Whenever minItems or maxItems is configured, the number of selected records is validated automatically through the new ItemCount property validator, without the need to add it to propertyValidators .

There is also a new property validator:

  • IntegerList checks whether all elements in the comma-separated list are integers.

Impact 

Form definitions can be set up to allow editors the selection of multiple database records and then render them using custom logic.

To avoid conflicts with existing configurations, minItems is set to 0 and maxItems to 1 by default.

Feature: #94886 - Show selected item counts in FormEngine 

See forge#94886

Description 

The backend FormEngine now displays a compact badge for fields configured via TCA types and renderTypes when they use item count validation rules such as minItems and maxItems .

The badge reflects the currently selected number of items and the configured minimum or maximum values, and it updates automatically when entries are added or removed. This improves the visibility of selection limits for supported field types including select, category, group, folder and inline elements.

When the maximum allowed number of items is reached, the max-items notice continues to be shown as before. If a field requires a minimum number of selected items, the badge also indicates that the current selection has not yet reached the required value.

Impact 

Editors get immediate visual feedback about how many items are currently selected in relation to the configured minItems and maxItems limits. The badge is injected on the client side for any field exposing item count validation rules, so it also applies to custom render types without requiring additional PHP code.

Feature: #107345 - Language filter for the recycler module 

See forge#107345

Description 

The recycler backend module now provides a language switcher in the module doc header, comparable to the one in the record history module.

Selecting a language restricts the list of deleted records to that specific language. The relevant language field is resolved per table through the TCA schema Language capability, so tables that use a custom language field name are handled correctly.

Tables that are not language aware are omitted from the result while a language is selected. Choosing the [All] entry shows all deleted records, independent of their language or language awareness.

The selected language is persisted as module data for the backend user, so the chosen language context is remembered across requests, just like the depth and table selection. The language dropdown is only shown when there are at least two languages present, otherwise it defaults to showing all records.

Additionally, the language icon and name is now shown inside the recycler record listing for each record that is language aware and has a language set (also -1 is evaluated as all languages).

Impact 

Editors can now narrow down deleted records to a single language in the recycler module, making it easier to locate and restore translated content.

Feature: #109444 - Add default value support for CountrySelect form element 

See forge#109444

Description 

The TYPO3 Form Framework's CountrySelect element now supports a defaultValue property. Previously only prioritizedCountries , onlyCountries , and excludeCountries could be configured — there was no way to pre-select a country when the form is initially rendered.

The default value can be set in two ways:

  • Directly in the YAML form definition.
  • Through the new Inspector-CountrySingleSelectEditor in the Form Editor backend module.

YAML form definition example:

type: CountrySelect
identifier: country-1
label: 'Country'
defaultValue: 'DE'
properties:
  onlyCountries:
    - DE
    - AT
    - CH
Copied!

The new inspector editor is a single-select country dropdown that appears in the Form Editor's inspector panel. It lists all available countries regardless of any configured onlyCountries , excludeCountries , or prioritizedCountries filters. A description hint reminds editors to ensure the selected country is not excluded by the configured country filters.

If the configured defaultValue refers to a country that is excluded by a country filter, the default value has no visible effect on the frontend because the corresponding <option> element will not be rendered.

On the frontend the defaultValue is resolved through the existing form framework property binding, so no additional rendering logic is required.

Impact 

Integrators and editors can now pre-select a country for CountrySelect form elements. The default value can be configured via YAML or through the Form Editor backend module without writing custom code.

Feature: #109811 - PSR-14 event AfterFormStateInitializedEvent 

See forge#109811

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterFormStateInitializedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterFormStateInitialized'] .

The new event is dispatched by FormRuntime after the FormState has been restored from the request. At this point both the form state (submitted values) and the static form definition are available, which makes it particularly suitable for enriching components that need runtime data (e.g. configuring property mapping for file uploads).

The event provides the following public properties:

  • $formRuntime : The form runtime object (read-only).
  • $request : The current request (read-only).

Example 

An example event listener could look like this:

EXT:my_extension/Classes/EventListener/MyAfterFormStateInitializedEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterFormStateInitializedEvent;

#[AsEventListener(identifier: 'my-extension/after-form-state-initialized')]
final readonly class MyAfterFormStateInitializedEventListener
{
    public function __invoke(AfterFormStateInitializedEvent $event): void
    {
        // Access $event->formRuntime->getFormState() here
    }
}
Copied!

Impact 

With the new AfterFormStateInitializedEvent , it is now possible to react to the form state being fully initialized using the standard PSR-14 event listener mechanism.

Feature: #109836 - PSR-14 event before form value is rendered 

See forge#109836

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormValueIsRenderedEvent has been introduced. It is dispatched by RenderFormValueViewHelper right before the value of a single form element is rendered, which means before the data is assigned to the Fluid variable provider.

The event carries the following properties:

  • $data : The data array that is assigned to the Fluid variable ( element , value , processedValue , isMultiValue , or element and isSection for sections). It can be modified, and additional keys can be added for use in custom templates.
  • $element : The form element being rendered (read-only).
  • $formRuntime : The current form runtime (read-only), which gives access to the form definition and to the values of all other form elements.

This allows developers to introduce additional data processing for complex individual form fields.

Example 

The corresponding event listener class:

EXT:my_extension/Classes/Form/EventListener/BeforeFormValueIsRenderedEventListener.php
<?php

namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormValueIsRenderedEvent;

final class BeforeFormValueIsRenderedEventListener
{
    #[AsEventListener('my_extension/form/before-form-value-is-rendered')]
    public function __invoke(BeforeFormValueIsRenderedEvent $event): void
    {
        if (!$event->element instanceof MyCustomSelect) {
            return;
        }
        if ($option = $this->customRepository->findOneByValue($event->data['value'])) {
            $event->data['processedValue'] = $option->getLabel();
        }
    }
}
Copied!

Impact 

With this change, it is possible to do the required manipulation in a single EventListener class instead of requiring to override multiple Fluid templates.

Feature: #109849 - PSR-14 Event after form definition validation config built 

See forge#109849

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterFormDefinitionValidationConfigurationIsBuiltEvent has been introduced as a replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] .

The event is dispatched by \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService after the form definition validation configuration has been built from the form editor setup. It allows event listeners to add additional writable property paths for custom form editor inspector editor implementations that do not declare their writable property paths via the standard YAML configuration (e.g. propertyPath ).

The event provides the following API:

  • getPrototypeName(): string – The prototype name for which the configuration was built.
  • getConfiguration(): array – The built validation configuration.
  • setConfiguration(array $configuration): void – Replace the validation configuration.

Example 

An example event listener that adds an additional writable property path for a custom form element type:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterFormDefinitionValidationConfigurationIsBuiltEvent;

#[AsEventListener(
    identifier: 'my-extension/after-form-definition-validation-configuration-is-built',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterFormDefinitionValidationConfigurationIsBuiltEvent $event): void
    {
        $configuration = $event->getConfiguration();
        $configuration['formElements']['MyCustomElement']['additionalPropertyPaths'][]
            = 'properties.my.custom.property';
        $event->setConfiguration($configuration);
    }
}
Copied!

Impact 

With the new AfterFormDefinitionValidationConfigurationIsBuiltEvent , it is now possible to extend the form definition validation configuration using the modern PSR-14 event listener API.

Feature: #110259 - New PSR-14 AfterFileListRowPreparedEvent 

See forge#110259

Description 

A new PSR-14 event \TYPO3\CMS\Filelist\Event\AfterFileListRowPreparedEvent has been added to TYPO3 Core. This event is fired after a file or folder row has been fully prepared for the Media module, right before it is rendered into the final table row markup.

Unlike \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent , which only allows modifying the action icons in the control column, this event provides access to the already-rendered data of every column in the row (for example name , size or any additional metadata column added via the column selector), as well as the row's HTML tag attributes. This closes a gap compared to the classic record list in Content > Records, which has offered an equivalent event ( \TYPO3\CMS\Backend\RecordList\Event\AfterRecordListRowPreparedEvent ) for its own rows.

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\Filelist\Event\AfterFileListRowPreparedEvent;

final class AfterFileListRowPreparedEventListener
{
    #[AsEventListener('my-extension/after-filelist-row-prepared')]
    public function __invoke(AfterFileListRowPreparedEvent $event): void
    {
        $data = $event->getData();
        // Modify the already-rendered value of a specific column
        $data['my_column'] = 'My custom value';
        $event->setData($data);
    }
}
Copied!

Impact 

Extension authors can now decorate or override the rendered value of any column - not just the action icons - for a file or folder row in the File List module, without resorting to hooks or class-name-based reflection workarounds.

Feature: #110269 - Allow row updater registration with PHP attribute 

See forge#110269

Description 

The new PHP attribute \TYPO3\CMS\Core\Attribute\AsRowUpdater has been added, which registers a class implementing \TYPO3\CMS\Core\Upgrades\RowUpdater\RowUpdaterInterface as a row updater for the databaseRowsUpdateWizard upgrade wizard.

Row updaters are executed by the "Execute database row updates" upgrade wizard. It iterates over all rows of all TCA tables once and hands each row to every registered row updater that announced a potential update for the table, which makes them the tool of choice for data migrations that cannot be expressed as a plain SQL statement.

Until now, the list of row updaters was a hard-coded, TYPO3 Core internal property of \TYPO3\CMS\Core\Upgrades\DatabaseRowsUpdateWizard . Extension and project developers had no supported way to add their own implementation. With the new attribute, registration is a matter of tagging the class:

EXT:my_extension/Classes/Upgrades/RowUpdater/MyRowUpdater.php
namespace MyVendor\MyExtension\Upgrades\RowUpdater;

use TYPO3\CMS\Core\Attribute\AsRowUpdater;
use TYPO3\CMS\Core\Upgrades\RowUpdater\RowUpdaterInterface;

#[AsRowUpdater('myExtensionMyRowUpdater')]
final class MyRowUpdater implements RowUpdaterInterface
{
    public function getTitle(): string
    {
        return 'Migrate my_field of tx_myextension_domain_model_entity records';
    }

    public function hasPotentialUpdateForTable(string $tableName): bool
    {
        return $tableName === 'tx_myextension_domain_model_entity';
    }

    public function updateTableRow(string $tableName, array $row): array
    {
        $row['my_field'] = $this->migrateValue($row['my_field']);
        return $row;
    }
}
Copied!

The identifier is optional. If it is omitted, the fully qualified class name is used as identifier instead:

EXT:my_extension/Classes/Upgrades/RowUpdater/MyRowUpdater.php
#[AsRowUpdater]
final class MyRowUpdater implements RowUpdaterInterface
Copied!

Registration can alternatively be done manually with the service tag install.rowupdater , for example when the row updater class cannot carry the attribute:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\Upgrades\RowUpdater\MyRowUpdater:
    tags:
      - name: 'install.rowupdater'
        identifier: 'myExtensionMyRowUpdater'
Copied!

The identifier must be unique across all registered row updaters. It is stored in the system registry once the row updater has been executed and is used to determine which row updaters still need to run, so it should not be changed afterwards.

Impact 

Extensions and projects can now ship their own row updaters and have them executed by the "Execute database row updates" upgrade wizard, both in the Admin Tools > Upgrade backend module and with the upgrade:run console command.

Registered row updaters are listed with their title, can be marked as done or undone individually, and are picked up automatically - no further configuration is needed beyond the attribute or the service tag.

Feature: #110277 - Register file renderers as tagged services 

See forge#110277

Description 

File renderers, used for example by the <f:media> ViewHelper to render audio, video or online media files, are now registered as tagged services via dependency injection instead of the previous programmatic registration through \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry->registerRendererClass() in ext_localconf.php.

A file renderer class implementing \TYPO3\CMS\Core\Resource\Rendering\FileRendererInterface is registered by adding the new PHP attribute \TYPO3\CMS\Core\Attribute\AsFileRenderer to the class:

EXT:my_extension/Classes/Resource/Rendering/MyVideoRenderer.php
use TYPO3\CMS\Core\Attribute\AsFileRenderer;
use TYPO3\CMS\Core\Resource\Rendering\FileRendererInterface;

#[AsFileRenderer(priority: 10)]
final class MyVideoRenderer implements FileRendererInterface
{
    // ...
}
Copied!

The renderer priority is defined at registration time via the attribute. Renderers with a higher priority are asked first whether they can render a given file ( canRender() ). The file renderers shipped with TYPO3 Core are registered with the default priority 0 (previously 1 via the removed getPriority() method), so any custom renderer using a priority above 0 takes precedence over them. The order in which renderers of the same priority are evaluated is not defined and must not be relied upon; use distinct priorities if the evaluation order matters.

Alternatively, the service tag fal.file_renderer can be used directly in Configuration/Services.yaml:

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\Resource\Rendering\MyVideoRenderer:
  tags:
    - name: fal.file_renderer
      priority: 10
Copied!

Impact 

Registering file renderers as tagged services has the following benefits over the previous programmatic registration:

  • The registration is resolved once at container compile time instead of executing registration code from ext_localconf.php on every request. Loading and validating the renderer classes per request, as well as instantiating and sorting them by priority at runtime, is not necessary anymore.
  • File renderers are proper services now and can use dependency injection in their constructor.
  • The renderer priority is declared at the class itself instead of depending on the loading order of ext_localconf.php files.
  • The registration is validated at container compile time: a service tagged as fal.file_renderer that does not implement FileRendererInterface fails the container build with a speaking exception, instead of causing errors when a file is rendered.

Registration in ext_localconf.php via RendererRegistry->registerRendererClass() is not evaluated anymore, see Breaking: #110277 - File renderer registration and interface changed for the upgrade path.

Feature: #110286 - Register text extractors as tagged services 

See forge#110286

Description 

Text extractors, used to extract the textual content from files, for example for search indexing, are now registered as tagged services via dependency injection instead of the previous programmatic registration through \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorRegistry->registerTextExtractor() in ext_localconf.php.

A text extractor class implementing \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorInterface is registered by adding the new PHP attribute \TYPO3\CMS\Core\Attribute\AsTextExtractor to the class:

EXT:my_extension/Classes/Resource/TextExtraction/PdfTextExtractor.php
use TYPO3\CMS\Core\Attribute\AsTextExtractor;
use TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorInterface;

#[AsTextExtractor(priority: 10)]
final class PdfTextExtractor implements TextExtractorInterface
{
    // ...
}
Copied!

The extractor priority is defined at registration time via the attribute. Extractors with a higher priority are asked first whether they can extract text from a given file ( canExtractText() ). The PlainTextExtractor shipped with TYPO3 Core is registered with the default priority 0 , so any custom extractor using a priority above 0 takes precedence. The order in which extractors of the same priority are evaluated is not defined and must not be relied upon; use distinct priorities if the evaluation order matters.

Alternatively, the service tag fal.text_extractor can be used directly in Configuration/Services.yaml:

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\Resource\TextExtraction\PdfTextExtractor:
  tags:
    - name: fal.text_extractor
      priority: 10
Copied!

Impact 

Registering text extractors as tagged services has the following benefits over the previous programmatic registration:

  • The registration is resolved once at container compile time instead of executing registration code from ext_localconf.php on every request. Loading and validating the extractor classes per request as well as instantiating them at runtime is not necessary anymore.
  • Text extractors are proper services now and can use dependency injection in their constructor.
  • The extractor priority is declared at the class itself instead of depending on the loading order of ext_localconf.php files.
  • The registration is validated at container compile time: a service tagged as fal.text_extractor that does not implement TextExtractorInterface fails the container build with a speaking exception, instead of causing errors when text is extracted.

Registration in ext_localconf.php via TextExtractorRegistry->registerTextExtractor() is not evaluated anymore, see Breaking: #110286 - Text extractor registration and interface changed for the upgrade path.

Feature: #110287 - Register meta tag managers as tagged services 

See forge#110287

Description 

Meta tag managers, used to manage and render the <meta> tags of a page, are now registered as tagged services via dependency injection instead of the previous programmatic registration through \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry->registerManager() in ext_localconf.php.

A meta tag manager class implementing \TYPO3\CMS\Core\MetaTag\MetaTagManagerInterface (usually by extending \TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager ) is registered by adding the new PHP attribute \TYPO3\CMS\Core\Attribute\AsMetaTagManager to the class:

EXT:my_extension/Classes/MetaTag/MyMetaTagManager.php
use TYPO3\CMS\Core\Attribute\AsMetaTagManager;
use TYPO3\CMS\Core\MetaTag\AbstractMetaTagManager;

#[AsMetaTagManager(identifier: 'my-manager')]
final class MyMetaTagManager extends AbstractMetaTagManager
{
    // ...
}
Copied!

The manager order is defined at registration time via the optional before and after attribute arguments, referencing the identifiers of other managers. A manager without constraints is ordered before the generic manager shipped with TYPO3 Core, which handles every property and therefore always comes last — this matches the previous default of registerManager() .

#[AsMetaTagManager(identifier: 'my-manager', before: ['opengraph'], after: ['html5'])]
Copied!

Alternatively, the service tag metatag.manager can be used directly in Configuration/Services.yaml, with before and after given as comma-separated lists:

EXT:my_extension/Configuration/Services.yaml
MyVendor\MyExtension\MetaTag\MyMetaTagManager:
  tags:
    - name: metatag.manager
      identifier: 'my-manager'
Copied!

Impact 

Registering meta tag managers as tagged services has the following benefits over the previous programmatic registration:

  • The registration and the resolution of the before / after ordering constraints happen once at container compile time instead of executing registration code from ext_localconf.php and sorting the managers on every request.
  • Meta tag managers are proper services now and can use dependency injection in their constructor.
  • The registration is validated at container compile time: a service tagged as metatag.manager that does not implement MetaTagManagerInterface , or is missing the identifier tag attribute, fails the container build with a speaking exception.

Registration in ext_localconf.php via MetaTagManagerRegistry->registerManager() is not evaluated anymore, see Breaking: #110287 - Meta tag manager registration and interface changed for the upgrade path.

Feature: #110347 - Native lazy objects for Extbase lazy loading 

See forge#110347

Description 

Extbase now uses native PHP lazy objects (available since PHP 8.4) to implement lazy loading of relations in domain models annotated with the #[Extbase\ORM\Lazy] attribute.

Previously, two dedicated proxy classes were used, which are now deprecated:

  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage for 1:n and m:n relations
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy for 1:1 and n:1 relations

The DataMapper now creates the following constructs instead:

  • For 1:n and m:n relations, a lazy ghost instance of the regular \TYPO3\CMS\Extbase\Persistence\ObjectStorage class. The storage fetches its content from the persistence layer on first access.
  • For 1:1 and n:1 relations, a lazy proxy instance of the actual target entity class. The related record is fetched on first access, and the proxy then transparently forwards all calls to the mapped entity.

Concept 

A native lazy object is a real instance of its class: A lazy loaded Category parent is a Category , and a lazy loaded ObjectStorage is an ObjectStorage . The PHP engine tracks the initialization state and invokes an initializer on first property access. There is no foreign proxy class anymore that only mimics the API of the real object.

The uid of a lazy 1:1 or n:1 relation is available without database access, since it is known from the parent's database row and set eagerly on the uninitialized proxy. Dirty checking, URI generation and form rendering therefore do not trigger relation loading anymore.

Benefits 

  • instanceof checks against the target entity class are now true for uninitialized lazy relations.
  • Native property types can be used for lazy properties. The union type workaround is no longer needed:

    // Before
    #[Extbase\ORM\Lazy]
    protected Category|LazyLoadingProxy|null $parent = null;
    
    public function getParent(): ?Category
    {
        if ($this->parent instanceof LazyLoadingProxy) {
            $this->parent->_loadRealInstance();
        }
        return $this->parent;
    }
    
    // After
    #[Extbase\ORM\Lazy]
    protected ?Category $parent = null;
    
    public function getParent(): ?Category
    {
        return $this->parent;
    }
    Copied!
  • Type declarations of methods consuming such relations can rely on the actual model class.
  • Lazy relations no longer carry references to the DataMapper or the parent object as object state.

Serialization 

Serializing Extbase entities and object storages is now well-defined:

  • Serializing a lazy relation initializes it first and serializes the actual entity data. Previously, the internal proxy state (raw field value, property name and a back reference to the whole parent object graph, in older TYPO3 versions even the DataMapper state) was serialized.
  • ObjectStorage now implements __serialize() and __unserialize() . The contained objects are stored as a plain list and the internal spl_object_hash() based bookkeeping is rebuilt on unserialize() . Calling detach() , contains() or offsetGet() on an unserialized storage now works correctly. Previously, the storage silently kept stale object hashes of the original process and those methods did not work on the restored storage.
  • Payloads serialized with older TYPO3 versions can still be unserialized.

Impact 

Lazy loading works transparently for domain models using the #[Extbase\ORM\Lazy] attribute. No migration is required: Existing models keep working, the attribute remains the single way to declare a lazy relation.

Extension authors may optionally simplify their models:

  • Union type declarations like Category|LazyLoadingProxy|null can be reduced to the actual entity type, for example ?Category .
  • Calls to LazyLoadingProxy->_loadRealInstance() and instanceof LazyLoadingProxy checks in getters can be removed, the property value is an instance of the target class in all cases.
  • Whether a lazy object has been initialized can be determined with native PHP reflection:

    $isUninitialized = new \ReflectionClass(ObjectStorage::class)
        ->isUninitializedLazyObject($storage);
    Copied!

Behavioral notes:

  • Calling count() on an uninitialized lazy object storage now fully initializes the storage with one query. Previously, a dedicated COUNT query was executed without initializing the storage.
  • If a lazy 1:1 or n:1 relation points to a record that cannot be resolved anymore (for example a deleted or hidden record), the parent property is reset to null on first access, as before. Code holding the proxy instance itself encounters an empty entity instance instead of the previous null return value of _loadRealInstance() .

Feature: #110453 - Derive PageRepository instances for another Context 

See forge#110453

Description 

\TYPO3\CMS\Core\Domain\Repository\PageRepository is bound to a Context when it is created. That Context decides which language overlay and which workspace the resolved records belong to. Code that needed records for a different language - a link to a translated page, an hreflang tag, a menu rendered without overlays - had to clone the global Context, set the language aspect on the clone, and then create a new instance by hand:

$context = clone GeneralUtility::makeInstance(Context::class);
$context->setAspect('language', $languageAspect);
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
Copied!

This forced every consumer to build the object itself, which prevented PageRepository from being injected as a regular service.

Two methods now derive a new instance from an existing one:

public function withContext(Context $context): self
public function withLanguageAspect(LanguageAspect $languageAspect): self
Copied!

withLanguageAspect() keeps every other aspect - workspace, visibility, frontend user - of the current Context and only exchanges the language aspect. withContext() replaces the Context as a whole and returns the same instance if the given Context is already the active one.

Both methods leave the original instance untouched, so a single injected PageRepository can serve as the starting point for any number of derived ones:

class MyLinkGenerator
{
    public function __construct(
        private readonly PageRepository $pageRepository,
    ) {}

    public function getTranslatedPage(int $pageId, LanguageAspect $languageAspect): array
    {
        return $this->pageRepository->withLanguageAspect($languageAspect)->getPage($pageId);
    }
}
Copied!

Passing new LanguageAspect() resolves records without applying any language overlay at all.

Impact 

Extension authors can inject PageRepository through dependency injection and derive Context-specific instances where they are needed, instead of cloning a Context and calling GeneralUtility::makeInstance(PageRepository::class, $context) .

The existing constructor is unchanged and keeps working.

Deprecation: #110148 - Experimental backend ViewHelpers 

See forge#110148

Description 

The following experimental backend-related Fluid ViewHelpers have been marked as deprecated:

  • \TYPO3\CMS\Fluid\ViewHelpers\Be\PagePathViewHelper ( <f:be.pagePath> )
  • \TYPO3\CMS\Fluid\ViewHelpers\Be\AbstractBackendViewHelper

The PagePathViewHelper rendered the current page path as displayed in TYPO3 backend modules. This information is nowadays part of the module doc header, which is rendered by \TYPO3\CMS\Backend\Template\ModuleTemplate within the corresponding controller.

The abstract AbstractBackendViewHelper provided the helper methods getModuleTemplate() and getPageRenderer() . Both are obsolete with the current ModuleTemplate view strategy and dependency injection, and the class is no longer used as a base class within TYPO3 Core.

Impact 

Using the <f:be.pagePath> ViewHelper in a Fluid template will trigger a PHP E_USER_DEPRECATED error.

Extending AbstractBackendViewHelper or calling its methods getModuleTemplate() or getPageRenderer() will trigger a PHP E_USER_DEPRECATED error.

Both classes will be removed in TYPO3 v16.0.

Affected installations 

All installations using the <f:be.pagePath> ViewHelper in backend Fluid templates, or custom backend ViewHelpers extending AbstractBackendViewHelper .

The extension scanner reports any usage of the affected classes as strong match.

Migration 

For the page path, use the doc header provided by \TYPO3\CMS\Backend\Template\ModuleTemplate in your backend controller, which already displays the current page path.

Custom backend ViewHelpers should extend \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper directly and retrieve \TYPO3\CMS\Backend\Template\ModuleTemplate or \TYPO3\CMS\Core\Page\PageRenderer via dependency injection instead of the removed helper methods.

Deprecation: #110202 - StringUtility::multibyteStringPad() method 

See forge#110202

Description 

The method \TYPO3\CMS\Core\Utility\StringUtility::multibyteStringPad() has been marked as deprecated and will be removed in TYPO3 v16.0.

The method was introduced to provide a multibyte-safe variant of PHP's str_pad() . Since PHP 8.3, the native function mb_str_pad() covers exactly this use case, making the TYPO3 wrapper obsolete.

Impact 

Calling the method will trigger a PHP deprecation warning. It will continue to work as before until it is removed in TYPO3 v16.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call StringUtility::multibyteStringPad() are affected.

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

Migration 

Use the native PHP function mb_str_pad() instead.

Note that mb_str_pad() throws a \ValueError when an empty pad string is passed, whereas StringUtility::multibyteStringPad() returned the input unchanged. If an empty pad string can occur, guard against it.

Before (deprecated)
use TYPO3\CMS\Core\Utility\StringUtility;

$padded = StringUtility::multibyteStringPad($string, 10, $padString, STR_PAD_LEFT);
Copied!
After (recommended)
$padded = $padString === ''
    ? $string
    : mb_str_pad($string, 10, $padString, STR_PAD_LEFT);
Copied!

Deprecation: #110285 - DataHandler->setCorrelationId() 

See forge#110285

Description 

The method \TYPO3\CMS\Core\DataHandling\DataHandler->setCorrelationId() has been marked as deprecated and will be removed in TYPO3 v16.0.

The correlation id of a DataHandler operation can now be handed over directly to DataHandler->start() as fifth argument. This also ensures that the correlation id is passed on to internally spawned sub instances of the DataHandler, so all record history entries of one logical operation share the same correlation scope. Setting the correlation id via the setter after start() did not propagate it to sub instances and is therefore superseded.

Impact 

Calling DataHandler->setCorrelationId() triggers a PHP E_USER_DEPRECATED error.

The extension scanner detects usages of the deprecated method as weak match.

Affected installations 

All installations with custom extensions calling DataHandler->setCorrelationId() , usually to group the record history entries of one logical operation. This is a rarely used API method.

Migration 

Pass the \TYPO3\CMS\Core\DataHandling\Model\CorrelationId instance directly to DataHandler->start() instead:

// Before
$dataHandler->start($dataMap, $commandMap);
$dataHandler->setCorrelationId($myCorrelationId);
$dataHandler->process_datamap();

// After
$dataHandler->start($dataMap, $commandMap, correlationId: $myCorrelationId);
$dataHandler->process_datamap();
Copied!

Deprecation: #110334 - AbstractXmlSitemapDataProvider 

See forge#110334

Description 

The class \TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider has been marked as deprecated and will be removed in TYPO3 v16.0.

XML sitemap data providers are services now, receiving all runtime information as \TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRequest argument of getSitemap() , see Breaking: #110334 - XML sitemap data provider interface changed. The base class only serves the previous approach of handing over the runtime information as constructor arguments and storing the items of a sitemap in a property.

The class is kept in TYPO3 v15 to ease the transition: Data providers extending it are still instantiated with their runtime information and keep working unchanged, which allows extensions to support TYPO3 v14 and v15 with a single implementation.

Impact 

Instantiating a data provider extending AbstractXmlSitemapDataProvider triggers a PHP E_USER_DEPRECATED error. The sitemap itself is rendered as before.

The extension scanner detects usages of the deprecated class as strong match.

Affected installations 

All installations with custom extensions providing an own XML sitemap data provider based on AbstractXmlSitemapDataProvider .

Migration 

Implement \TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface directly instead of extending the base class. The items are collected in getSitemap() instead of the constructor, and the properties of the base class are replaced by the XmlSitemapRequest argument:

EXT:my_extension/Classes/XmlSitemap/MyXmlSitemapDataProvider.php
// Before
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider;

class MyXmlSitemapDataProvider extends AbstractXmlSitemapDataProvider
{
    public function __construct(ServerRequestInterface $request, string $key, array $config = [], ?ContentObjectRenderer $cObj = null)
    {
        parent::__construct($request, $key, $config, $cObj);
        $itemRepository = GeneralUtility::makeInstance(MyItemRepository::class);
        foreach ($itemRepository->findAll($this->config) as $item) {
            $this->items[] = [
                'uid' => $item['uid'],
                'lastMod' => $item['tstamp'],
            ];
        }
    }

    protected function defineUrl(array $data): array
    {
        $data['loc'] = $this->cObj->createUrl([
            'parameter' => $data['uid'],
            'forceAbsoluteUrl' => 1,
        ]);
        return $data;
    }
}
Copied!
EXT:my_extension/Classes/XmlSitemap/MyXmlSitemapDataProvider.php
// After
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemap;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRequest;

final readonly class MyXmlSitemapDataProvider implements XmlSitemapDataProviderInterface
{
    public function __construct(
        private MyItemRepository $itemRepository,
    ) {}

    public function getSitemap(XmlSitemapRequest $sitemapRequest): XmlSitemap
    {
        $items = [];
        foreach ($this->itemRepository->findAll($sitemapRequest->configuration) as $item) {
            $items[] = [
                'uid' => $item['uid'],
                'lastMod' => $item['tstamp'],
            ];
        }
        return XmlSitemap::forPage(
            $items,
            $sitemapRequest->page,
            fn(array $item): array => $this->defineUrl($item, $sitemapRequest),
        );
    }

    private function defineUrl(array $item, XmlSitemapRequest $sitemapRequest): array
    {
        $item['loc'] = $sitemapRequest->contentObjectRenderer->createUrl([
            'parameter' => $item['uid'],
            'forceAbsoluteUrl' => 1,
        ]);
        return $item;
    }
}
Copied!

The properties of the base class are mapped as follows:

  • $this->key becomes $sitemapRequest->name
  • $this->config becomes $sitemapRequest->configuration
  • $this->request becomes $sitemapRequest->request
  • $this->cObj becomes $sitemapRequest->contentObjectRenderer
  • $this->items becomes the items handed over to XmlSitemap::forPage()
  • $this->numberOfItemsPerPage becomes the fourth argument of XmlSitemap::forPage()

The methods getKey() , getItems() , getLastModified() and getNumberOfPages() are not needed anymore: The returned XmlSitemap provides the last modification date and the number of pages, both calculated from the items of the sitemap.

Registering a sitemap in TypoScript is unchanged: Data providers are still referenced by their class name.

Extensions supporting TYPO3 v14 and v15 with a single implementation keep extending AbstractXmlSitemapDataProvider and migrate once support for TYPO3 v14 is dropped.

Deprecation: #110347 - LazyLoadingProxy and LazyObjectStorage 

See forge#110347

Description 

The internal Extbase lazy loading classes

  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy
  • \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage
  • \TYPO3\CMS\Extbase\Persistence\Generic\LoadingStrategyInterface

have been marked as deprecated. Lazy relations of domain model properties annotated with #[Extbase\ORM\Lazy] are created as native PHP lazy objects by the DataMapper instead: 1:n and m:n relations as lazy ghost instances of \TYPO3\CMS\Extbase\Persistence\ObjectStorage , 1:1 and n:1 relations as lazy proxy instances of the actual target entity class.

Impact 

Instantiating LazyLoadingProxy or LazyObjectStorage will trigger a PHP E_USER_DEPRECATED error. All three classes will be removed in TYPO3 v16.0.

TYPO3 Core does not create instances of these classes anymore. Existing instanceof checks against them evaluate to false for relations created by the DataMapper.

Affected installations 

Installations with extensions that reference these classes, for example in union type declarations of lazy model properties, in instanceof checks, or by calling LazyLoadingProxy->_loadRealInstance() .

The extension scanner reports usages as weak match.

Migration 

No change is required for declaring lazy relations, the #[Extbase\ORM\Lazy] attribute remains the single way to do so.

Union type declarations can be reduced to the actual entity type:

// Before
#[Extbase\ORM\Lazy]
protected Category|LazyLoadingProxy|null $parent = null;

// After
#[Extbase\ORM\Lazy]
protected ?Category $parent = null;
Copied!

Calls to _loadRealInstance() can be removed altogether, since the property value is an instance of the target entity class in all cases and initializes itself transparently on first access:

// Before
public function getParent(): ?Category
{
    if ($this->parent instanceof LazyLoadingProxy) {
        $this->parent->_loadRealInstance();
    }
    return $this->parent;
}

// After
public function getParent(): ?Category
{
    return $this->parent;
}
Copied!

Code that needs to know whether a lazy object has been initialized (for example to avoid triggering database queries) can use native PHP reflection:

$reflection = new \ReflectionClass(ObjectStorage::class);
if ($reflection->isUninitializedLazyObject($storage)) {
    // storage content has not been fetched from the database yet
}
Copied!

Deprecation: #110348 - AssetCollector media handling 

See forge#110348

Description 

The media related methods of \TYPO3\CMS\Core\Page\AssetCollector have been marked as deprecated and will be removed in TYPO3 v16.0:

  • AssetCollector->addMedia()
  • AssetCollector->getMedia()
  • AssetCollector->hasMedia()
  • AssetCollector->removeMedia()

Unlike JavaScript and stylesheet assets, collected media never contributed anything to the rendered output. The registry is a leftover of the $TSFE->imagesOnPage property, which was moved to the AssetCollector in TYPO3 v10.3 (see Deprecation: #90522 - TSFE properties regarding images) and only ever served the "Images on this page" section of the admin panel.

Collecting this information by having every image renderer report into a central registry is fragile: it only ever covered the code paths that remembered to call addMedia() . TYPO3 dispatches \TYPO3\CMS\Core\Resource\Event\AfterFileProcessingEvent for every processed file, which is a complete and more precise source for the same information.

Impact 

Calling one of the deprecated methods triggers a PHP E_USER_DEPRECATED error.

TYPO3 Core does not populate the registry anymore. The two places that used to do so no longer call addMedia() :

  • \TYPO3\CMS\Extbase\Service\ImageService->applyProcessingInstructions()
  • \TYPO3\CMS\Frontend\ContentObject\ImageContentObject (the IMAGE content object)

Consequently AssetCollector->getMedia() only returns entries that were added by third-party code.

The admin panel keeps its "Images on this page" section. It now collects the data through a PSR-14 listener on AfterFileProcessingEvent , which also reports correct file sizes for images stored in remote storages and adds the image dimensions.

Affected installations 

Installations with extensions calling any of the deprecated methods, and installations relying on Core populating the registry. Custom image ViewHelpers or content objects that mirrored the Core behaviour by calling addMedia() are the most likely candidates.

The methods are not covered by the extension scanner: their names are too generic to be matched reliably.

Migration 

Remove calls to addMedia() and removeMedia() — they serve no rendering purpose.

To collect the images processed during a request, register a PSR-14 event listener instead. This works regardless of whether an image is rendered through Fluid, TypoScript or custom code:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Event\AfterFileProcessingEvent;
use TYPO3\CMS\Core\SingletonInterface;

final class MyImageCollector implements SingletonInterface
{
    private array $images = [];

    #[AsEventListener('my-extension/collect-images')]
    public function collect(AfterFileProcessingEvent $event): void
    {
        $processedFile = $event->getProcessedFile();
        $publicUrl = $processedFile->getPublicUrl();
        if ($publicUrl !== null) {
            $this->images[$publicUrl] = $processedFile;
        }
    }

    public function getImages(): array
    {
        return $this->images;
    }
}
Copied!

Note that the event fires for every processed file of a request, including files that are processed but never emitted into the output.

Deprecation: #110477 - CommandRegistry->getSchedulableCommands() 

See forge#110477

Description 

The method \TYPO3\CMS\Core\Console\CommandRegistry->getSchedulableCommands() has been marked as deprecated and will be removed in TYPO3 v16.0.

Impact 

Calling the method triggers a PHP E_USER_DEPRECATED error.

Affected installations 

Installations with custom extensions that call CommandRegistry->getSchedulableCommands() are affected. The method is not used by TYPO3 Core.

The extension scanner reports usages as a weak match.

Migration 

Use CommandRegistry->getSchedulableCommandsConfiguration() to retrieve the configuration indexed by command identifier. If command instances are needed, retrieve them individually with CommandRegistry->get() :

foreach (array_keys($commandRegistry->getSchedulableCommandsConfiguration()) as $commandIdentifier) {
    $command = $commandRegistry->get($commandIdentifier);
}
Copied!

Deprecation: #110479 - ActionMenuItemGroup defaultController argument 

See forge#110479

Description 

The unused defaultController argument of the <f:be.menus.actionMenuItemGroup> ViewHelper has been deprecated and will be removed in TYPO3 v16.0.

Impact 

Using the defaultController argument triggers a PHP E_USER_DEPRECATED error.

Affected installations 

TYPO3 installations with Fluid templates that pass the defaultController argument to <f:be.menus.actionMenuItemGroup> are affected.

Migration 

Remove the unused defaultController argument from the ViewHelper call.

Important: #35069 - addQueryString.exclude also applies to config.linkVars 

See forge#35069

Description 

Both addQueryString and config.linkVars carry query parameters of the current request over into generated page links. Until now addQueryString.exclude only removed a parameter from the former, so a parameter listed in config.linkVars was silently added back to the link and could not be dropped at all:

config.linkVars = print(int)

lib.link = TEXT
lib.link {
    value = A link without the print parameter
    typolink {
        parameter.data = page:uid
        addQueryString = untrusted
        addQueryString.exclude = print
    }
}
Copied!

With a request to /some-page?print=1 the generated link still contained ?print=1.

addQueryString.exclude is now honored for parameters contributed by config.linkVars as well, so the link above no longer carries the print parameter. The exclusion is only applied when addQueryString is active — config.linkVars on its own is unaffected.

Integrators who relied on config.linkVars overruling addQueryString.exclude need to remove the affected parameter from the exclude list.

Important: #106584 - Asset and image ViewHelper arguments are registered 

See forge#106584

Description 

The ViewHelpers <f:asset.css> , <f:asset.script> and <f:image> read the arguments href , src and alt from the tag attributes instead of registering them. They were therefore missing from the generated XSD schema and from the ViewHelper reference, which made IDEs report them as unknown attributes.

All three are now registered as optional string arguments. Templates do not need to be changed, and the arguments keep behaving as before.

One detail changes in the rendered markup of <f:image> : an explicitly given alt attribute used to be written as the first attribute of the <img> tag, because unregistered arguments are added to the tag builder before the ViewHelper renders. It is now written after src , width and height , which is the position the fallback to the "alternative" metadata property has always used:

<!-- before -->
<img alt="alternative text" src="image.jpg" width="400" height="300" />

<!-- after -->
<img src="image.jpg" width="400" height="300" alt="alternative text" />
Copied!

The attribute order carries no meaning in HTML, so this only affects tests and tooling that compare rendered markup as a string.

Important: #110501 - Slashes in page titles no longer end up in generated slugs 

See forge#110501

Description 

The slug field of the pages table is generated from the page title. Until now, a slash contained in the title was kept as-is.

As a result, a page titled Terms / Conditions was given the slug /terms-/-conditions, and a page titled Support 24/7 was given the slug /support-24/7. The latter additionally introduces a path segment that does not exist as a page: the URL looks as if the page were a sub page 7 below a page /support-24.

The TCA of pages now configures a slug generator replacement that turns a slash into the fallback character -:

typo3/sysext/core/Configuration/TCA/pages.php
'slug' => [
    'config' => [
        'type' => 'slug',
        'generatorOptions' => [
            'fields' => ['title'],
            'fieldSeparator' => '/',
            'prefixParentPageSlug' => true,
            'replacements' => [
                '/' => '-',
            ],
        ],
        'fallbackCharacter' => '-',
        // ...
    ],
],
Copied!

The generated slugs therefore change as follows:

Page title Slug (before) Slug (now)
Terms / Conditions /terms-/-conditions /terms-conditions
Terms/Conditions /terms/conditions /terms-conditions
Support 24/7 /support-24/7 /support-24-7

Every slug generated from now on is affected, no matter whether it is built when a new page is created or when the slug is recreated from the title in the page properties. Slugs of existing pages are only changed once they are regenerated.

Manually entered slugs keep their slashes as before: a slash is a valid character in a page slug, it separates the path segments of a page.

Sites relying on the previous behaviour can restore it by unsetting the new option in Configuration/TCA/Overrides/pages.php:

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

unset($GLOBALS['TCA']['pages']['columns']['slug']['config']['generatorOptions']['replacements']);
Copied!

Important: #110540 - File collector sorting uses an enum 

See forge#110540

Description 

The sorting order passed to \TYPO3\CMS\Frontend\Resource\FileCollector->sort() is no longer a plain string but the new enum \TYPO3\CMS\Frontend\Resource\FileCollectionSorting with the cases Ascending , Descending and Random .

FileCollector is marked as @internal , so this is not treated as a breaking change. Extensions calling sort() directly must nevertheless adapt their code:

// before
$fileCollector->sort('title', 'descending');

// after
$fileCollector->sort('title', FileCollectionSorting::Descending);

// when the value comes from configuration or user input
$fileCollector->sort(
    'title',
    FileCollectionSorting::fromKeyword($direction),
);
Copied!

FileCollectionSorting::fromKeyword() resolves both the short and the long keywords ( asc , ascending , desc , descending , rand , random ) case-insensitively, and throws a \ValueError for anything else.

The TypoScript option sorting.direction of the FILES content object and of the files data processor keeps accepting the same keywords as before. An unknown keyword is no longer silently treated as "ascending" though, but now results in a \ValueError .

15.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 v14 

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

Also available 

14.3.x Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.3.0 LTS release.

Features 

Deprecation 

Important 

Important: #59007 - Indexed Search indexes files of storages that aren't public 

See forge#59007

Description 

With config.index_externals = 1 , Indexed Search follows links to files in the rendered page and indexes their content. So far this only worked for files of a storage marked as public, because those are linked with a directly accessible URL such as fileadmin/download.pdf.

Files of a storage that is not public are delivered through the file dump eID script instead, resulting in a link like index.php?eID=dumpFile&t=f&f=17&token=…. Such links could not be resolved to a file and were silently skipped, even though the file was readable on the server side.

These URLs are now resolved back to the FAL object they point to, and the file is made available locally for the external file parsers. As a result, PDF, Word or plain text documents of storages that are not public are indexed as well. The indexed URL still points to the file dump URL, so search results keep linking through the dump script.

Only file dump URLs carrying a valid token are taken into account, which means just those generated by TYPO3 itself.

Important: #81619 - stdWrap "override" applies the value 0 

See forge#81619

Description 

The stdWrap property override replaced the current content whenever its value was truthy after trimming. Since the string 0 is falsy in PHP, an override evaluating to a single zero silently kept the previous content instead of replacing it. TypoScript delivers every value as a string, which made 0 the one scalar an override could not produce, for example when the value came from a condition or from a nested stdWrap.

The property now leaves the content untouched only if the configured value is unset, empty or consists of whitespace, and applies every other value, 0 included.

Instances that relied on override = 0 being ignored - for example to keep a disabled override in place - now render 0 instead of the previous content. Remove the property or guard it with override.if to keep the previous output.

Important: #83788 - Select field filters ignore diacritics 

See forge#83788

Description 

The client side filters of a selectMultipleSideBySide and of a selectTree field compared the typed text against the labels exactly as both were written. Typing "creme" therefore did not find an option labelled "Crème brûlée", and a category tree did not narrow to a category whose title carries an accent.

Both the filter text and the labels are now folded before being compared: the value is lowercased, NFD splits an accented character into its base character plus a combining mark, and the marks of the combining diacritical marks block are removed.

What is removed are the marks a script writes optionally: the combining diacritical marks of Latin, Greek and Cyrillic, the Hebrew points and the Arabic harakat. An Arabic label carrying its harakat is therefore found when it is typed without them, and the alef variants fold onto the plain alef.

The marks that Thai, Devanagari and Japanese write obligatorily are kept, because there a mark separates one word from another rather than accenting it. Removing them would fold the Japanese for "school" and for "cuckoo" into one value and leave a Thai filter narrowing nothing.

The Latin letters that Unicode does not decompose are spelled out instead, so that an alphabet folds as a whole rather than by half: "ß" is compared as "ss", "æ" as "ae", "ø" as "o", "ł" as "l", "þ" as "th" and so on. Typing "strasse" therefore finds "Straße", and "lodz" finds "Łódź". Only the spellings that are conventional are applied -- the ones that are a matter of language, such as the German "ü" to "ue", are not, because the decomposition to "u" already settles them.

Since NFD also decomposes precomposed Hangul syllables into jamo, and both sides of the comparison are folded the same way, a Korean syllable now matches inside another one sharing its leading jamo.

Characters that a reader cannot see are no longer a reason for a label not to match: a soft hyphen and the zero width characters are dropped, and a no-break space and the other space variants are compared as a plain space. Both arrive in labels by pasting, and an editor has no way of telling that one is there.

Both steps live in the new module @typo3/core/utility/text-normalizer , which exports a TextNormalizer class with the static methods foldCaseAndDiacritics() and normalizeInvisibleCharacters() . That module is internal: it exists so that the filters of the backend treat their input the same way, and it is not a public JavaScript API for extensions yet.

Impact 

Editors searching a long option list or a large category tree find an entry whether or not they type its accents, in both directions: "creme" finds "Crème brûlée" and "crème" finds "Cremeschnitte".

The same comparison backs the predefined filter values of the multiSelectFilterItems TCA option, which are matched through the very same code path. A filter value configured with a diacritic in order to isolate exactly the accented options now also matches the unaccented ones.

Important: #88886 - Extbase persistence respects the language overlay type 

See forge#88886

Description 

The Extbase persistence layer previously applied "mixed" language overlay semantics at all times when overlaying records: If a record was not available in the requested language, the record in the default language was returned - regardless of the fallback type configured for the site language.

Extbase now respects the overlay type (derived from the fallbackType setting of the site language configuration) and the configured fallback chain when fetching aggregate roots and related child objects. Translation behavior of Extbase queries is now consistent with the regular translation behavior of pages and content in TYPO3:

  • fallbackType: strict : Records that are not translated into the requested language are not returned anymore. Repository->findByUid() on an untranslated record of the default language now returns null , and untranslated related child objects (for example categories or tags) are filtered from relations of a translated aggregate root.
  • fallbackType: fallback : Unchanged behavior. Records are overlaid with their translation if available, the default language record is used otherwise.
  • fallbackType: free : Unchanged behavior. No overlays are performed for regular queries. Identity lookups via Repository->findByUid() keep resolving translations using mixed overlay semantics.

Fetching a translated record directly by its uid (for example $postRepository->findByUid($uidOfTranslatedRecord) ) still returns the translated record, regardless of the language of the current context.

Note that records created through Extbase in the frontend - for example via form submissions - are persisted with sys_language_uid=0 (default language) unless a language is explicitly assigned. On sites using fallbackType: strict , such records are not visible in translated languages until they have been translated.

To keep the previous behavior for individual queries, a custom language aspect using mixed overlay semantics can be set on the query settings:

use TYPO3\CMS\Core\Context\LanguageAspect;

$querySettings = $query->getQuerySettings();
$languageAspect = $querySettings->getLanguageAspect();
$querySettings->setLanguageAspect(
    new LanguageAspect(
        $languageAspect->getId(),
        $languageAspect->getContentId(),
        LanguageAspect::OVERLAYS_MIXED,
        $languageAspect->getFallbackChain(),
    ),
);
Copied!

Important: #95714 - Null attributes are omitted in media rendering 

See forge#95714

Description 

The media file renderers

  • \TYPO3\CMS\Core\Resource\Rendering\AudioTagRenderer
  • \TYPO3\CMS\Core\Resource\Rendering\VideoTagRenderer
  • \TYPO3\CMS\Core\Resource\Rendering\VimeoRenderer
  • \TYPO3\CMS\Core\Resource\Rendering\YouTubeRenderer

now treat the values of the additionalAttributes option the same way Fluid's \TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder does:

  • null and false omit the attribute entirely
  • true renders a boolean attribute, the name without a value
  • An empty string renders name=""
  • Any other value renders name="value"

Up to now, null and false were both rendered as name="" . In the Vimeo and YouTube renderers, null additionally triggered the deprecation notice htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated .

This allows leaving out an attribute conditionally, which was not possible before:

<f:media file="{file}" additionalAttributes="{poster: posterUrl}" />
Copied!

If posterUrl is not set, the poster attribute is now dropped instead of being rendered as poster="" .

Attributes carrying an empty string are still rendered. In HTML, muted and muted="" mean the same thing, so dropping them would change the meaning of the generated markup.

Important: #100638 - Extbase ignoreEnableFields applies to overlays 

See forge#100638

Description 

The Extbase query setting setIgnoreEnableFields() switched off the enable fields for the records selected by the query, but not for the translations those records are overlaid with. A hidden translation was therefore never found, even though the query was told to ignore enable fields:

$query = $this->createQuery();
$query->getQuerySettings()->setIgnoreEnableFields(true);
return $query->execute();
Copied!

With languageOverlayMode = hideNonTranslated the record was dropped from the result entirely, while count() still counted it. With the default overlay mode the untranslated default language record was returned instead of the hidden translation.

Extbase now mirrors ignoreEnableFields and enableFieldsToBeIgnored into the visibility aspect of the context the overlay is done with, so hidden and time-restricted translations are found as well. Listing 'disabled' in setEnableFieldsToBeIgnored() reveals hidden translations, 'starttime' or 'endtime' reveals time-restricted ones, and an empty list keeps meaning "ignore all of them".

Frontend user group restrictions ( 'fe_group' ) are not part of the visibility aspect and are still applied to translations.

Extensions that use setIgnoreEnableFields(true) in a multi-language setup may see hidden translations in their result sets where they previously saw the default language record or no record at all.

Important: #101699 - Messenger wildcard routes act as a fallback 

See forge#101699

Description 

The Symfony Messenger transport routing configured via $GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] maps a message type to one or more transport identifiers.

So far a wildcard route - a namespace wildcard ( My\Message\* ) or the global wildcard '*' - was always applied in addition to any matching specific route. A message that matched both a specific route and a wildcard route was therefore dispatched to both transports. Because TYPO3 ships a default catch-all route '*' => 'default' , every routed message was silently sent to the default transport as well, and routing a message to its specific transport only required removing the wildcard route explicitly, typically in an extension's ext_localconf.php:

// previously required to avoid the additional wildcard routing
unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*']);
Copied!

A wildcard route now acts as a fallback: it is skipped as soon as a more specific type already matched, and only applies when no specific route matched. This mirrors the upstream Symfony SendersLocator resolution, so the shipped default '*' => 'default' route now behaves as a true fallback and the manual unset() of the wildcard route is no longer needed:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
    // handled by the "doctrine" transport only, the '*' route is skipped
    \My\Extension\Message\ImportMessage::class => 'doctrine',
    // fallback for every message without a more specific route
    '*' => 'default',
];
Copied!

Single and multiple transports per route 

A routing value may be given as a single transport identifier (a string) or as an ordered list of transport identifiers (an array) to route a single message to several transports on purpose:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
    // single transport
    \My\Extension\Message\ImportMessage::class => 'doctrine',
    // sent to "audit" and "doctrine", in that order
    \My\Extension\Message\AuditableInterface::class => ['audit', 'doctrine'],
    '*' => 'default',
];
Copied!

The legacy mapping syntax using a plain string ( \My\Message::class => 'doctrine' ) keeps working unchanged and is treated as the single-element array form ( \My\Message::class => ['doctrine'] ).

Wildcards are supported on every namespace level 

Wildcard routing is not limited to the global '*' route. A namespace wildcard may be configured on any level and is likewise treated as a fallback that is only used when no more specific route matched:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing'] = [
    // every message below \My\Extension\Message\Import\
    \My\Extension\Message\Import\* => 'import',
    // every other message below \My\Extension\Message\
    \My\Extension\Message\* => 'doctrine',
    // global fallback
    '*' => 'default',
];
Copied!

How a message is matched against the routing configuration 

The routing configuration is a plain lookup map; its key order is irrelevant. The order in which routes are considered comes from the type chain of the dispatched message, derived from the message's PHP type (via \Symfony\Component\Messenger\Handler\HandlersLocator::listTypes() ), not from the order of the entries in the global configuration array.

For a dispatched message the type chain is evaluated from most to least specific:

  1. the concrete message class,
  2. its parent classes (bottom-up),
  3. its implemented interfaces,
  4. the namespace wildcards for every namespace level (for example My\Extension\Message\Import\* , My\Extension\Message\* , My\Extension\* , My\* ),
  5. and finally the global wildcard '*' .

Senders are collected across every matching type in that order and deduplicated (first occurrence wins). The first non-wildcard match therefore "locks in" the transports and every following wildcard route is skipped. This resolution depends solely on the class, parent and interface hierarchy of the message and is entirely independent of the shape or ordering of the ['SYS']['messenger']['routing'] array - the configuration only provides the mapping from a type identifier to its transports.

Impact 

Installations that (knowingly or not) relied on a message being routed to both its specific transport and the wildcard transport - most notably the default '*' => 'default' route - will see such a message routed to its specific transport only. Configure the additional transport explicitly as a list value ( \My\Message::class => ['specific', 'default'] ) where the previous fan-out is still wanted.

Providing this change on older TYPO3 versions 

The change is contained in the core \TYPO3\CMS\Core\Messenger\TransportLocator . Projects that need the fallback resolution on an older TYPO3 version that does not ship it yet (for example TYPO3 v12) can obtain it in one of two ways.

Replace the core implementation via Symfony dependency injection 

Copy the current TransportLocator into an extension, for example as \MyVendor\MyExtension\Messenger\FallbackTransportLocator , keeping the #[AutowireLocator('messenger.sender', indexAttribute: 'identifier')] constructor argument. Then re-point the messenger senders-locator to it in the extension's Configuration/Services.yaml:

services:
  MyVendor\MyExtension\Messenger\FallbackTransportLocator:
    arguments:
      $sendersLocator: !tagged_locator { tag: 'messenger.sender', index_by: 'identifier' }

  # replace the core implementation used for message routing
  TYPO3\CMS\Core\Messenger\TransportLocator:
    alias: MyVendor\MyExtension\Messenger\FallbackTransportLocator

  Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface:
    alias: MyVendor\MyExtension\Messenger\FallbackTransportLocator
Copied!

Because the messenger senders-locator is resolved through this alias, the extension implementation is used instead of the core one and the fallback resolution becomes available on the older core version. If the WebhookTypesProvider display of the resolved transports is required as well, copy its adjusted version accordingly.

Backport the change via a Composer patch 

Alternatively, apply the change to typo3/cms-core directly using a Composer patch tool such as cweagans/composer-patches:

{
    "require": {
        "cweagans/composer-patches": "^1.7"
    },
    "extra": {
        "patches": {
            "typo3/cms-core": {
                "Messenger wildcard routing fallback": "patches/messenger-wildcard-fallback.patch"
            }
        }
    }
}
Copied!

The patch file should contain the diff of typo3/sysext/core/Classes/Messenger/TransportLocator.php (and, if used, the WebhookTypesProvider.php adjustment as a typo3/cms-webhooks patch file) and is applied automatically on composer install .

Important: #106903 - Form element value resolution in finishers 

See forge#106903

Description 

Finisher options substitute {elementIdentifier} placeholders with the submitted value of that form element. Until now the raw submitted value was used everywhere, so a select, radio button, multi checkbox or country select element contributed its stored option key instead of the label an editor had configured and translated - the subject of an email read Request from mr where the summary table below it read Request from Mister .

Options that are read by a human now resolve the placeholder through the form element itself and therefore use the translated option label:

  • EmailFinisher : subject , title , message and senderName
  • ConfirmationFinisher : message
  • FlashMessageFinisher : messageBody and messageTitle

The same resolution already backed the summary page and the email body through the RenderFormValue ViewHelper, so both places now agree on what a submitted value looks like.

Every other finisher option keeps the submitted value, because it ends up in a stored record, a query or a URL, where the option key is what matters. That includes the e-mail addresses next to the sender name - senderAddress , recipients , replyToRecipients , carbonCopyRecipients and blindCarbonCopyRecipients - and the redirect targets pageUid , additionalParameters and fragment .

The SaveToDatabaseFinisher in particular never resolves labels. Both of its mapping routes store the submitted value: elements.<element> writes it directly, and databaseColumnMappings.<column>.value resolves a {elementIdentifier} placeholder to the same value, so a column filled through either route carries the same content. whereClause matches against it accordingly. A stored option key stays independent of the site language the form was submitted in and of later label changes, and remains usable for queries and joins - resolve labels when reading the records instead.

A custom finisher opts a single option into label resolution by reading it with AbstractFinisher->parseOptionAsDisplayValue() instead of parseOption() .

Two further behaviours changed along the way:

  • An array value interpolated into a longer string - a multi select inside Request for {salutation} {name} - previously aborted the finisher with a FinisherException carrying code 1519239265 . Such a value is now joined with , instead. An array that contains an object which cannot be converted to a string still throws, now with code 1787754756 .
  • The implementationClassName of the SingleSelect , MultiSelect , RadioButton and MultiCheckbox elements changed to GenericOptionableFormElement , and the one of CountrySelect to CountrySelect . Both extend GenericFormElement , so an instanceof check against it keeps matching.

Affected installations 

Installations using EXT:form whose email subject, confirmation message or flash message references a select, radio button, multi checkbox or country select element, and installations that catch FinisherException with code 1519239265 .

Migration 

No migration is required. Code catching 1519239265 no longer needs that branch.

Important: #107032 - AsMessageHandler "fromTransport" property is now honoured 

See forge#107032

Description 

Symfony Messenger's \Symfony\Component\Messenger\Attribute\AsMessageHandler attribute provides a fromTransport property to bind a message handler to a single receiving transport: the handler is then only executed for messages that were received from that transport by a worker ( typo3 messenger:consume <transport> ).

So far TYPO3 collected the attribute but dropped the fromTransport value while building the handler map, so the property had no effect and a handler declaring it was executed for the message regardless of the transport the message was received from. This made a proper asynchronous send/receive setup - where a message is consumed from a dedicated transport and only the matching handler runs - impossible to express.

The value is now passed through to the handler descriptor and evaluated against the envelope's \Symfony\Component\Messenger\Stamp\ReceivedStamp , matching the upstream Symfony behaviour:

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

// Only runs for MyMessage instances received from the "async" transport.
#[AsMessageHandler(fromTransport: 'async')]
final readonly class MyAsyncHandler
{
    public function __invoke(MyMessage $message): void
    {
        // ...
    }
}
Copied!

Resolution rules:

  • A handler without fromTransport keeps running for every matching message, regardless of the receiving transport (unchanged behaviour).
  • A handler with fromTransport only runs when the message carries a ReceivedStamp for one of the bound transports. A message dispatched and handled synchronously (no ReceivedStamp , i.e. not received from a worker) is not affected by the restriction and still reaches the handler.

Binding a handler to multiple transports 

A handler may be bound to more than one receiving transport. As the Symfony AsMessageHandler attribute only accepts a single transport name per attribute, several transports are expressed by repeating the attribute (it is \Attribute::IS_REPEATABLE ):

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

// Runs for MyMessage received from the "async" or the "priority" transport.
#[AsMessageHandler(fromTransport: 'async')]
#[AsMessageHandler(fromTransport: 'priority')]
final readonly class MyAsyncHandler
{
    public function __invoke(MyMessage $message): void
    {
        // ...
    }
}
Copied!

When a handler is registered via a service tag instead of the attribute, the fromTransport tag option additionally accepts a list of transport names ( string , list<string> or null ):

services:
  MyVendor\MyExtension\Messenger\MyAsyncHandler:
    tags:
      - name: 'messenger.message_handler'
        fromTransport: [ 'async', 'priority' ]
Copied!

Duplicate and empty transport names are ignored, and the handler runs at most once per received message even when several bindings match.

Impact 

Installations that already annotated a handler with fromTransport and (knowingly or not) relied on the value being ignored will see that handler no longer run for messages received from a different transport. Remove the fromTransport property from such a handler to restore the previous "handle everything" behaviour.

Providing this change on older TYPO3 versions 

The fix spans two core classes:

  • \TYPO3\CMS\Core\Messenger\HandlersLocatorFactory stores the transport binding and builds one handler descriptor per receiving transport, and
  • \TYPO3\CMS\Core\DependencyInjection\MessageHandlerPass reads the fromTransport tag option and carries it into that factory.

The factory is a regular service and can be swapped through Configuration/Services.yaml. The pass, however, is a Symfony compiler pass registered in core's Configuration/Services.php - it is not a service and cannot be replaced through Services.yaml. As the fromTransport value is already dropped inside that pass on an older core, replacing only the factory is not sufficient: the value never reaches it.

Because a second, corrected compiler pass registered from an extension would run in addition to the core one (re-adding the handler unbound and defeating the restriction), a local extension can only provide the corrected factory, not the corrected pass. A complete backport therefore requires a Composer patch that adjusts both classes in typo3/cms-core.

Backport the change via a Composer patch 

Apply the change to typo3/cms-core using a Composer patch tool such as cweagans/composer-patches:

{
    "require": {
        "cweagans/composer-patches": "^1.7"
    },
    "extra": {
        "patches": {
            "typo3/cms-core": {
                "Honour AsMessageHandler fromTransport": "patches/messenger-from-transport.patch"
            }
        }
    }
}
Copied!

The patch file should contain the diff of both typo3/sysext/core/Classes/Messenger/HandlersLocatorFactory.php and typo3/sysext/core/Classes/DependencyInjection/MessageHandlerPass.php, and is applied automatically on composer install .

Optionally replace the factory in a local extension 

If the corrected pass is provided by a Composer patch, the corrected factory may instead live in a local extension. Copy the current HandlersLocatorFactory into the extension, for example as \MyVendor\MyExtension\Messenger\HandlersLocatorFactory , and re-point the core service to it in the extension's Configuration/Services.yaml:

services:
  MyVendor\MyExtension\Messenger\HandlersLocatorFactory: ~

  # replace the core factory the messenger handlers locator is built from
  TYPO3\CMS\Core\Messenger\HandlersLocatorFactory:
    alias: MyVendor\MyExtension\Messenger\HandlersLocatorFactory
Copied!

Because MessageHandlerPass resolves the factory definition by the core class name (and the alias resolves to the extension definition), the compiler pass adds its addHandler() calls to the extension implementation.

Important: #108813 - Record icons accept record objects 

See forge#108813

Description 

Rendering the icon of a record required a plain database row and its table name. Code already working with objects implementing \TYPO3\CMS\Core\Domain\RecordInterface had to rebuild an array just to render an icon, although such a record already knows its own table.

\TYPO3\CMS\Core\Imaging\IconFactory therefore provides a new method getIconForRecordObject() , which takes the record object instead of a table name and a row:

// Plain database row
$icon = $iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL);

// Record object
$icon = $iconFactory->getIconForRecordObject($record, IconSize::SMALL);
Copied!

The Fluid ViewHelper <core:iconForRecord> gained a new argument record , which is interchangeable with row . Both accept a plain database row as well as a record object, and exactly one of them must be given. The argument table is no longer mandatory: it is required for row arrays only and is not evaluated for record objects.

Before
<core:iconForRecord table="tt_content" row="{CType: item.CType}" />
Copied!
After
<core:iconForRecord record="{record}" />
Copied!

The record's raw database row is used to determine both the type icon and the status overlays, so overlays for disabled or scheduled records are rendered the same way as for a plain row array.

Impact 

Existing usages with a table name and a plain array continue to work unchanged. Extension authors can now pass record objects directly, which is the recommended way when the surrounding code already works with records.

Important: #109672 - Translation domain syntax supported in resourceOverrides 

See forge#109672

Description 

The translation domain syntax (introduced in TYPO3 v14.0, see Feature: #93334 - Translation Domain Mapping) can now also be used as the key in the $GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides'] configuration.

Previously, only file-based paths were accepted as keys. Now both the traditional file path and the short domain syntax are valid:

// File path syntax (still supported)
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']
    ['EXT:core/Resources/Private/Language/locallang_common.xlf'][]
    = 'EXT:my_extension/Resources/Private/Language/locallang_common_override.xlf';

// Domain syntax (new)
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']['core.common'][]
    = 'EXT:my_extension/Resources/Private/Language/locallang_common_override.xlf';
Copied!

Language-specific overrides also support domain syntax:

// Language-specific override with domain syntax
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']['de']['core.common'][]
    = 'EXT:my_extension/Resources/Private/Language/de.locallang_common_override.xlf';
Copied!

Impact 

Existing configurations using file-based keys continue to work without changes. The domain syntax is an optional, shorter alternative that removes the need to know the exact file path of the language file being overridden.

Important: #109731 - Modal API: composable size configuration 

See forge#109731

Description 

The backend Modal API ( @typo3/backend/modal ) accepts an additional shape for the size option of Modal.advanced() . In addition to the existing Sizes enum ( small , default , medium , large , full , expand ), callers may now pass a SizeConfig object that configures width and height independently.

The existing enum-based size values continue to work unchanged. No migration is required for existing call sites.

New types 

A new export is available from @typo3/backend/modal :

  • Size – an enum of per-axis size tokens that map to width and height values: small , default , medium , large , full .

The SizeConfig type uses these tokens:

type SizeConfig = { width?: Size; height?: Size };
Copied!

Usage 

Compose width and height independently:

import Modal, { Size } from '@typo3/backend/modal';

Modal.advanced({
  title: 'New page',
  content: '...',
  size: {
    width: Size.medium,
    height: Size.large,
  },
});
Copied!

Override only one axis (the other defaults to the modal's intrinsic size):

Modal.advanced({
  // ...
  size: { width: Size.medium },
});
Copied!

Important: #109787 - Add files to livesearch results 

See forge#109787

Description 

The backend live search now surfaces files from the file abstraction layer alongside pages and database records. A new live search provider queries the sys_file and sys_file_metadata tables through the existing \TYPO3\CMS\Core\Resource\Search\FileSearchQuery , so matches against file names as well as indexed metadata fields like title, description or alternative text appear in the global search.

Results respect the user's file mounts via the existing FolderMountsRestriction , so non-admin users only see files within their permitted scope.

The result detail panel has been extended to show a thumbnail for previewable files (images, videos, PDFs and online media such as YouTube or Vimeo) together with a list of properties such as the location, file size and last modification date.

New API on ResultItem 

To support richer detail rendering, two optional fields have been added to \TYPO3\CMS\Backend\Search\LiveSearch\ResultItem :

  • setThumbnailUrl(?string $thumbnailUrl) — a URL to a preview image rendered in the detail panel; falls back to the icon when null.
  • addProperty(string $label, string $value) — a key/value pair rendered as a description list in the detail panel. Useful for surfacing metadata like timestamps, sizes or relations.

Both fields default to empty, so existing providers continue to work unchanged.

Impact 

Backend users can now find files directly through the global live search without switching to the media module. Custom providers can opt into the new thumbnail and property fields to enrich their detail views.

Important: #110233 - Search query added to AfterPageTreeItemsPreparedEvent 

See forge#110233

Description 

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent , dispatched by \TYPO3\CMS\Backend\Controller\Page\TreeController after the backend page tree items have been resolved and prepared, now carries the current page tree search query explicitly, instead of requiring event listeners to extract it themselves from GET parameters of the current PSR-7 request.

A new method getSearchQuery(): ?string has been added, returning the search phrase used to filter the page tree, or null if no search is currently active.

Note that the $request argument - and therefore the return value of getRequest() - is now nullable as well, since the event can be dispatched in contexts where no PSR-7 request instance is available in future.

Impact 

Custom event listeners that read the page tree search phrase via $event->getRequest()->getQueryParams()['q'] should be adapted to use the new $event->getSearchQuery() method instead.

Listeners should furthermore no longer assume that getRequest() always returns an instance of ServerRequestInterface , since it may be null in future major versions.

Important: #110307 - New editRecord ViewHelper argument for contextual editing 

See forge#110307

Description 

A new optional boolean argument contextual has been added to the Fluid ViewHelper <be:link.editRecord> .

When enabled, the ViewHelper renders a <typo3-backend-contextual-record-edit-trigger> element instead of a classic edit link. It builds a URL for the record_edit_contextual backend route and a fallback URL for the classic record_edit route. It also loads the required JavaScript module @typo3/backend/element/contextual-record-edit-trigger.js , so custom backend modules do not need to register that asset manually.

This makes the contextual editing behavior already used in the layout module available to backend Fluid templates without custom PHP markup.

Existing usages of <be:link.editRecord> remain unchanged.

Usage Example 

Example usage of contextual argument
<be:link.editRecord
    uid="{record.uid}"
    table="pages"
    fields="title,subtitle"
    contextual="true"
    class="btn btn-default"
>
    Edit page properties
</be:link.editRecord>
Copied!

Impact 

Extension authors can use contextual record editing in backend Fluid templates, for example in custom backend previews, while keeping the shared URL-building logic of <be:link.editRecord> .

Important: #110358 - Site configuration caches resolved site data 

See forge#110358

Description 

TYPO3 now caches all data needed to create Site entity objects in the "core" cache: the site configuration enriched with route enhancers provided by site sets, the composed site settings, information about invalid sets, as well as the contents of the optional per-site files csp.yaml, setup.typoscript, constants.typoscript and page.tsconfig. Previously, this data was recomputed and re-read from the file system on every request.

The Site entity objects themselves are still created at runtime once per request: Expressions of configured base variants ( baseVariants ) continue to be evaluated per request and environment as before.

Any change performed via the TYPO3 backend (Site Management modules) or via the SiteWriter API invalidates this cache automatically.

However, changes applied directly to the files csp.yaml, setup.typoscript, constants.typoscript or page.tsconfig within a site configuration folder (for example config/sites/<identifier>/) now require flushing all caches to become active - as was already the case for config.yaml and settings.yaml before.

Important: #110385 - Language pack state stored in JSON file 

See forge#110385

Description 

The "last updated" timestamps of downloaded language packs used to be stored in the sys_registry database table. When language packs are updated as part of a deployment, the database connection may not yet be available at that stage, which led to errors.

These timestamps are now stored in a JSON file, language_pack_states.json, next to the downloaded language packs themselves, in var/labels/ (Composer-based installations) or typo3conf/l10n/ (classic installations). Existing entries are migrated from sys_registry to this file once, using the upgrade wizard Migrate the "languagePacks" registry entries to JSON storage.

This is relevant for deployment workflows that commit the downloaded language packs to version control instead of triggering a language pack update as part of every deployment: language_pack_states.json must be committed (or otherwise provisioned) together with the language pack files it describes. Otherwise, the backend's "Manage Language Packs" module will not display accurate "last updated" information for packs that already exist on disk.

Important: #110591 - Custom columns in the workspaces module 

See forge#110591

Description 

Listeners of the PSR-14 event \TYPO3\CMS\Workspaces\Event\AfterDataGeneratedForWorkspaceEvent can enrich the records displayed in the workspaces backend module, but the module rendered a fixed set of columns and discarded any additional data. Adding custom columns to the module, which was possible until TYPO3 v7, therefore did not work.

The record table of the workspaces module now renders custom columns. A listener declares one by adding an additional section to the records it wants to enrich:

$data = $event->getData();
$data['tt_content:42']['additional']['deadline'] = [
    'label' => 'Deadline',
    'value' => '2026-03-01 08:00',
    'icon' => 'actions-clock',
    'title' => 'Editorial deadline',
    'url' => '/typo3/module/my-module?element=42',
];
$event->setData($data);
Copied!

Records are indexed by table name and uid of the versioned record. All keys of a column are optional:

  • label - header of the column, falls back to the column identifier
  • value - content of the cell, rendered as plain text
  • icon - identifier of an icon rendered in front of the value
  • title - title attribute of the cell
  • url - wraps the content of the cell into a link to this URL

A column is displayed as soon as one record declares it. The columns are determined from all records of the workspace and not only from those of the current page, so the table header does not change while paging.

14.3 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Deprecation: #107931 - Lowlevel DatabaseIntegrityCheck class 

See forge#107931

Description 

The class \TYPO3\CMS\Lowlevel\Integrity\DatabaseIntegrityCheck has been deprecated and will be removed in TYPO3 v15.0.

The class is no longer used internally by TYPO3 and should not be relied upon by extensions.

Impact 

Using DatabaseIntegrityCheck will trigger a PHP E_USER_DEPRECATED error. The class will be removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with extensions that use DatabaseIntegrityCheck directly.

Migration 

Extensions that rely on this class should implement the necessary functionality themselves.

Deprecation: #109107 - CacheAction key "href" 

See forge#109107

Description 

The CacheAction array key href used in cache action definitions provided by the \TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent has been deprecated in favor of endpoint . The new key name better reflects the purpose of this field, which is used as an AJAX endpoint URL. The value must be a string .

Impact 

Cache action arrays that contain an href rather than an endpoint key will trigger a PHP E_USER_DEPRECATED notice. \TYPO3\CMS\Backend\Backend\ToolbarItems\ClearCacheToolbarItem will automatically migrate href to endpoint at runtime to maintain backward compatibility.

Support for the href key will be removed in TYPO3 v15.0.

Affected installations 

Any installation that has extensions which register custom cache actions with ModifyClearCacheActionsEvent and provide an action URL in the href array key.

Migration 

Replace the href key with endpoint in any cache action array returned from a ModifyClearCacheActionsEvent listener.

$event->addCacheAction([
    'id' => 'my_custom_cache',
-   'href' => $uriBuilder->buildUriFromRoute('ajax_my_cache_clear'),
+   'endpoint' => (string)$uriBuilder->buildUriFromRoute('ajax_my_cache_clear'),
    'iconIdentifier' => 'actions-system-cache-clear',
    'title' => 'Clear my cache',
    'description' => 'Optional description',
    'severity' => 'notice',
]);
Copied!

Deprecation: #109438 - ext_tables.php in extensions 

See forge#109438

Description 

Extensions that still ship an ext_tables.php file will now trigger a PHP E_USER_DEPRECATED error when the file is loaded during a non-cached request or cache warm-up.

The ext_tables.php file was historically used to register backend modules, page doktypes, user settings, and other runtime configuration. All of these use cases now have dedicated alternatives in modern TYPO3:

  • Backend modules: Configuration/Backend/Modules.php
  • Backend routes: Configuration/Backend/Routes.php
  • User settings: Configuration/TCA/Overrides/be_users.php (see forge#108843)
  • Page doktype allowed record types: Configuration/TCA/Overrides/pages.php (see forge#108557)

Impact 

A PHP E_USER_DEPRECATED error is triggered for every third-party extension that still provides an ext_tables.php file whenever ext_tables.php files are loaded without caching, for example during cache warm-up or in a development context.

Support for ext_tables.php will be removed in TYPO3 v15.0.

Affected installations 

All installations with third-party extensions that still ship an ext_tables.php file.

Migration 

Move all registration from ext_tables.php to the appropriate configuration files.

User settings 

User settings previously registered via TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings() in ext_tables.php should now be registered via TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserSetting() in Configuration/TCA/Overrides/be_users.php.

Before:

ext_tables.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

$GLOBALS['TYPO3_USER_SETTINGS']['columns']['myCustomSetting'] = [
    'type' => 'check',
    'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
];
ExtensionManagementUtility::addFieldsToUserSettings(
    'myCustomSetting',
    'after:emailMeAtLogin'
);
Copied!

After:

Configuration/TCA/Overrides/be_users.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addUserSetting(
    'myCustomSetting',
    [
        'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
        ],
    ],
    'after:emailMeAtLogin'
);
Copied!

Page doktype allowed record types 

Page doktypes previously registered via PageDoktypeRegistry->add() in ext_tables.php should now use the TCA option allowedRecordTypes in Configuration/TCA/Overrides/pages.php.

Before:

ext_tables.php
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
    \TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry::class
)->add(116, [
    'allowedTables' => ['tt_content', 'my_custom_record'],
]);
Copied!

After:

Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = [
    'tt_content',
    'my_custom_record',
];
Copied!

Once all registrations have been moved, the ext_tables.php file can be removed from the extension.

Deprecation: #109517 - PSR-14 event \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent  

See forge#109517

Description 

The PSR-14 event that is dispatched in the User Settings panel to allow injection of custom JavaScript methods has been deprecated:

  • \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent

With the integration of EXT:setup into EXT:backend (see Important: #109517 - Setup extension merged into backend extension) this event is now superseded by:

  • \TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent

For better dual version compatibility, no deprecation is emitted when using the legacy event location.

Migration to the new event can be done by just replacing its new name. For details, see Moved event AddJavaScriptModulesEvent.

Impact 

Using the old event will work as before in TYPO3 v14 but will be removed with TYPO3 v15.0.

To keep listeners working, the new event AddUserSettingsJavaScriptModulesEvent must be utilized instead.

Affected installations 

Instances and extensions that register a PSR-14 listener on AddJavaScriptModulesEvent .

Migration 

See Moved event AddJavaScriptModulesEvent. The new event name and class namespace can be used with no further functional changes.

Deprecation: #109519 - BackendUtility item list label methods 

See forge#109519

Description 

The following methods in \TYPO3\CMS\Backend\Utility\BackendUtility have been deprecated:

  • getLabelFromItemlist()
  • getLabelFromItemListMerged()
  • getLabelsFromItemsList()

Their logic has been moved to the new \TYPO3\CMS\Core\Schema\SchemaLabelResolver class, which provides proper dependency injection support.

Impact 

Calling these methods will trigger a PHP E_USER_DEPRECATED error. The methods will be removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with extensions that call BackendUtility::getLabelFromItemlist() , BackendUtility::getLabelFromItemListMerged() or BackendUtility::getLabelsFromItemsList() .

Migration 

Replace calls to \TYPO3\CMS\Backend\Utility\BackendUtility::getLabelFromItemlist() with \TYPO3\CMS\Core\Schema\SchemaLabelResolver->getLabelForFieldValue() .

Before:

use TYPO3\CMS\Backend\Utility\BackendUtility;

$label = BackendUtility::getLabelFromItemlist(
    $table, $column, $value, $row
);
Copied!

After:

use TYPO3\CMS\Core\Schema\SchemaLabelResolver;

$label = $this->schemaLabelResolver->getLabelForFieldValue(
    $table, $column, $value, $row
);
Copied!

Replace calls to \TYPO3\CMS\Backend\Utility\BackendUtility::getLabelFromItemListMerged() with \TYPO3\CMS\Core\Schema\SchemaLabelResolver->getLabelForFieldValue() .

Before:

use TYPO3\CMS\Backend\Utility\BackendUtility;

$label = BackendUtility::getLabelFromItemListMerged(
    $pageId, $table, $column, $value, $row
);
Copied!

After:

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\SchemaLabelResolver;

$columnTsConfig = BackendUtility::getPagesTSconfig($pageId)
    ['TCEFORM.'][$table . '.'][$column . '.'] ?? [];
$label = $this->schemaLabelResolver->getLabelForFieldValue(
    $table, $column, $value, $row, $columnTsConfig
);
Copied!

Replace calls to BackendUtility::getLabelsFromItemsList() with \TYPO3\CMS\Core\Schema\SchemaLabelResolver->getLabelsForFieldValues() . Note that the new method returns an array of raw labels instead of a comma-separated translated string — callers must handle translation and joining themselves.

Before:

use TYPO3\CMS\Backend\Utility\BackendUtility;

$labels = BackendUtility::getLabelsFromItemsList(
    $table, $column, $keyList, $columnTsConfig, $row
);
Copied!

After:

use TYPO3\CMS\Core\Schema\SchemaLabelResolver;

$labels = $this->schemaLabelResolver->getLabelsForFieldValues(
    $table, $column, $keyList, $row, $columnTsConfig
);
$translatedLabels = implode(
    ', ',
    array_map($languageService->sL(...), $labels)
);
Copied!

Deprecation: #109523 - GeneralUtility::isOnCurrentHost() without PSR-7 request 

See forge#109523

Description 

Calling \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost() without providing a PSR-7 \Psr\Http\Message\ServerRequestInterface as the second argument is deprecated.

The method previously resolved the current host via \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() , which hides an implicit dependency on server globals. The method signature has been extended to accept an explicit PSR-7 request object, which should be passed instead.

Impact 

A PHP E_USER_DEPRECATED error is triggered when \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost() is called without a ServerRequestInterface argument.

Affected installations 

All installations with third-party extensions that call \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost() with only one argument.

Migration 

Pass the current PSR-7 request as the second argument to \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost() .

Before:

use TYPO3\CMS\Core\Utility\GeneralUtility;

$isOnCurrentHost = GeneralUtility::isOnCurrentHost($url);
Copied!

After:

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$isOnCurrentHost = GeneralUtility::isOnCurrentHost($url, $request);
Copied!

The PSR-7 request is available in various places, for example as an argument in controller actions, via $GLOBALS['TYPO3_REQUEST'] in legacy contexts, and via ServerRequestInterface method parameters.

Deprecation: #109529 - Page module section markup events 

See forge#109529

Description 

The PSR-14 events that allow listeners to inject HTML before or after content elements have been rendered inside a backend layout column have been deprecated and will be removed in TYPO3 v15.0:

  • \TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent
  • \TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent
  • \TYPO3\CMS\Backend\View\Event\AbstractSectionMarkupGeneratedEvent

The accompanying methods GridColumn::getBeforeSectionMarkup() and GridColumn::getAfterSectionMarkup() have also been deprecated.

These events were introduced in TYPO3 v10.3 alongside the legacy PageLayoutView class to enrich backend layout columns with custom markup. They expose raw HTML strings as a column-level extension point and force every refactoring of the page module to keep emitting that markup at exactly the same position in the DOM. This makes it impossible to keep them stable across versions while improving the page module: ongoing work to rebuild the page module and its drag, drop and paste behavior cannot proceed without locking the column's internal structure to whatever the events happened to assume. Removing the events is a prerequisite for that work.

The only listener that still consumed the event in the core, PageLayoutViewDrawEmptyColposContent , has been removed. Its job — showing a placeholder block for backend layout cells that do not have a configured colPos — is now performed in the Fluid template PageLayout/Grid/Column.fluid.html via an {column.unassigned} condition. No extension action is required for this case.

Impact 

Calling \TYPO3\CMS\Backend\View\Event\AbstractSectionMarkupGeneratedEvent::setContent() from a listener will trigger a deprecation-level log entry. The classes and the two GridColumn getters will be removed in TYPO3 v15.0.

Existing listeners will keep functioning in TYPO3 v14: the dispatch sites in GridColumn still fire the events and the Fluid partials still render the resulting {column.beforeSectionMarkup} and {column.afterSectionMarkup} strings.

Affected installations 

Instances and extensions that register a PSR-14 listener on BeforeSectionMarkupGeneratedEvent or AfterSectionMarkupGeneratedEvent , or that call GridColumn::getBeforeSectionMarkup() / GridColumn::getAfterSectionMarkup() .

Migration 

There is no direct replacement. Listeners that decorated backend layout columns through these events should be removed.

Deprecation: #109544 - GeneralUtility::sanitizeLocalUrl() needs PSR-7 request 

See forge#109544

Description 

Calling \TYPO3\CMS\Core\Utility\GeneralUtility::sanitizeLocalUrl() without passing the current PSR-7 request as the second argument is deprecated. The method previously resolved host and site information via \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() , which falls back to server superglobals. Passing the request explicitly allows the method to read this information from \TYPO3\CMS\Core\Http\NormalizedParams instead.

Impact 

Calling \TYPO3\CMS\Core\Utility\GeneralUtility::sanitizeLocalUrl() with only one argument triggers a PHP E_USER_DEPRECATED error.

Affected installations 

All installations that call \TYPO3\CMS\Core\Utility\GeneralUtility::sanitizeLocalUrl() without passing a \Psr\Http\Message\ServerRequestInterface as the second argument.

The extension scanner will detect affected usages as a strong match.

Migration 

Pass the current PSR-7 request as the second argument:

use TYPO3\CMS\Core\Utility\GeneralUtility;

- $url = GeneralUtility::sanitizeLocalUrl($url);
+ $url = GeneralUtility::sanitizeLocalUrl($url, $request);
Copied!

Deprecation: #109548 - GeneralUtility::locationHeaderUrl() without PSR-7 request 

See forge#109548

Description 

Calling \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl() without providing a PSR-7 \Psr\Http\Message\ServerRequestInterface as the second argument is deprecated.

The method previously resolved the current host and request directory via \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() , which hides an implicit dependency on server globals. The method signature has been extended to accept an explicit PSR-7 request object, which should be passed instead.

Impact 

A PHP E_USER_DEPRECATED error is triggered when \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl() is called without a ServerRequestInterface argument.

Affected installations 

All installations that call \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl() without passing ServerRequestInterface as the second argument.

The extension scanner will detect affected usages as a strong match.

Migration 

Pass the current PSR-7 request as the second argument to \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl() .

use TYPO3\CMS\Core\Utility\GeneralUtility;
+ use Psr\Http\Message\ServerRequestInterface;

- $url = GeneralUtility::locationHeaderUrl($path);
+ $url = GeneralUtility::locationHeaderUrl($path, $request);
Copied!

The PSR-7 request is available in various places, for example as an argument in controller actions, via $GLOBALS['TYPO3_REQUEST'] in legacy contexts, and via ServerRequestInterface method parameters.

Deprecation: #109551 - GeneralUtility::getIndpEnv() 

See forge#109551

Description 

Method \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() has been deprecated.

The method abstracts server environment variables and was used to obtain request-related data from PHP superglobals, such as the current host, URI, and site path. This information is reliably available via \TYPO3\CMS\Core\Http\NormalizedParams , which is attached as an attribute to the PSR-7 request.

Impact 

Calling \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() triggers a PHP E_USER_DEPRECATED error.

Affected installations 

All installations that call \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() directly.

The extension scanner will detect affected usages as a strong match.

Migration 

Replace calls to \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv() with the corresponding \TYPO3\CMS\Core\Http\NormalizedParams getter. A NormalizedParams instance is available as an attribute of the PSR-7 request:

// Before
use TYPO3\CMS\Core\Utility\GeneralUtility;

$siteUrl = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
$host = GeneralUtility::getIndpEnv('HTTP_HOST');

// After
use TYPO3\CMS\Core\Http\NormalizedParams;

/** @var NormalizedParams $normalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');
$siteUrl = $normalizedParams->getSiteUrl();
$host = $normalizedParams->getHttpHost();
Copied!

Deprecation: #109575 - Various ContentObjectRenderer properties/methods 

See forge#109575

Description 

Several properties and a methods of \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer have been deprecated.

Properties 

ContentObjectRenderer->$lastTypoLinkResult  

The property held the \TYPO3\CMS\Frontend\Typolink\LinkResultInterface produced by the most recent createLink() call. Relying on a side-effect property that is overwritten on every subsequent link call is fragile. Use the return value of createLink() directly instead.

ContentObjectRenderer->$currentRecordNumber and ContentObjectRenderer->$parentRecordNumber  

These counters are incremented by ContentContentObject and RecordsContentObject while iterating over their record sets, and exposed to TypoScript via getData cobj:parentRecordNumber . They carry no known use case for third-party extensions and are deprecated.

ContentObjectRenderer->$checkPid_badDoktypeList  

The property was intended to cache a comma-separated list of page doctypes that should be excluded from link target checks, but it was never written to or read from any code path in TYPO3 itself.

Methods 

ContentObjectRenderer->readFlexformIntoConf()  

The method parses a FlexForm XML string or array into a flat TypoScript configuration array. It only covered the sDEF sheet and had no equivalent in TYPO3 core itself. Use \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::convertFlexFormContentToArray() to decode FlexForm data, and map the result into your own configuration structure as needed.

TypoScript getData type 

The cobj:parentRecordNumber type for the getData function is deprecated. It returned the value of $parentRecordNumber , which is now deprecated.

ContentObjectRenderer->getRequest() fallback to $GLOBALS['TYPO3_REQUEST']  

The getRequest() method falls back to $GLOBALS['TYPO3_REQUEST'] when no request had been set via setRequest() before. This fallback has been deprecated. Third-party code that instantiates ContentObjectRenderer must call setRequest(ServerRequestInterface $request) before calling start() or any other method that requires the request.

Impact 

Accessing $lastTypoLinkResult or $checkPid_badDoktypeList , calling readFlexformIntoConf() , evaluating the cobj:parentRecordNumber getData type, or triggering the $GLOBALS['TYPO3_REQUEST'] fallback in getRequest() all raise E_USER_DEPRECATED errors at runtime. The fallback will additionally throw an exception in TYPO3 v15 when no request has been set.

$currentRecordNumber and $parentRecordNumber carry only a docblock @deprecated annotation for now — they remain functional and do not raise runtime errors.

Affected installations 

Installations with extensions that:

  • Read $cObj->lastTypoLinkResult after calling createLink()
  • Read $currentRecordNumber , $parentRecordNumber , or $checkPid_badDoktypeList
  • Call $cObj->readFlexformIntoConf()
  • Use the cobj:parentRecordNumber getData type in TypoScript;
  • Instantiate ContentObjectRenderer without calling setRequest() before start() .

The extension scanner detects usages of the deprecated properties as weak matches.

Migration 

$lastTypoLinkResult  

Capture the return value of createLink() directly:

// Before
$cObj->createLink($linkText, $conf);
$result = $cObj->lastTypoLinkResult;

// After
$result = $cObj->createLink($linkText, $conf);
Copied!

setRequest() before start()  

Call setRequest() immediately after instantiation, before any other method:

// Before
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->start($data, $table);

// After
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start($data, $table);
Copied!

readFlexformIntoConf()  

Replace with FlexFormTools::convertFlexFormContentToArray() and map the decoded array into the configuration structure as needed:

// Before
$conf = [];
$cObj->readFlexformIntoConf($flexFormXml, $conf);

// After
$conf = $this->flexFormTools->convertFlexFormContentToArray($flexFormXml);
Copied!

All other deprecated items 

Remove all usages. None of the remaining deprecated properties, nor cobj:parentRecordNumber getData type have a replacement.

Important: #109107 - Cache action endpoints should return JSON response 

See forge#109107

Description 

AJAX endpoints registered as custom cache actions via \TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent should return a JSON response containing success , title , and message fields.

The clear-cache toolbar now treats a missing or non- false success value as a successful operation and falls back to generic notification labels when title or message are absent. While this keeps older endpoints working without changes, providing explicit values gives users meaningful, context-specific feedback and ensures error conditions are surfaced correctly.

Important: #109517 - Setup extension merged into backend extension 

See forge#109517

Description 

The system extension setup (typo3/cms-setup) existed for historical reasons as a separate package. It provided the "User Settings" backend module, where users could change their password, name, email, language, avatar and other personal preferences.

In modern web applications a user profile module should never be optional, so the extension has been fully merged into the backend extension (typo3/cms-backend). The module is now always available when the backend is installed and can still be hidden for individual users via user TSconfig. The separate package is no longer needed and should not be referenced in new installations.

For Composer-based installations 

The Composer package typo3/cms-backend now replaces typo3/cms-setup. This means:

  • There is no need to require typo3/cms-setup in composer.json anymore. Existing references are resolved automatically because typo3/cms-backend declares that it replaces the package.
  • No manual action is required during an upgrade – Composer handles the replacement transparently.
  • New projects should not add typo3/cms-setup as a dependency.

For class-based references 

The following public classes have been moved and class aliases are in place for backwards compatibility:

  • \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent \TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent
  • \TYPO3\CMS\Setup\Form\Element\AvatarElement \TYPO3\CMS\Backend\Form\Element\AvatarElement
  • \TYPO3\CMS\Setup\UserFunctions\UserSettingsItemsProcFunc \TYPO3\CMS\Backend\UserFunctions\UserSettingsItemsProcFunc

Extensions using the old class names will continue to work, but should be updated to the new namespaces.

Moved event AddJavaScriptModulesEvent 

A special case is \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent . This file has been moved to typo3/sysext/backend/DeprecatedClasses/Setup/Event/AddJavaScriptModulesEvent.php and is added as a specific PSR-4 autoload entry to the Core's composer.json map, so that the legacy event can be dispatched properly. No deprecation message is emitted when dispatching this legacy event.

In addition, a new event \TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent has been added with a distinguishing name. Both events are dispatched in TYPO3 v14, with the legacy event being deprecated and removed in TYPO3 v15 (see Deprecation: #109517 - PSR-14 event \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent).

Extensions providing compatibility to two versions should proceed as below:

For compatibility with TYPO3 v13 and v14 

Only listen to the legacy event \TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent :

EXT:my_extension/Classes/Listener/SetupModuleListener.php
<?php
declare(strict_types=1);

namespace MyExtension\Listener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Setup\Event\AddJavaScriptModulesEvent;

final class SetupModuleListener
{
    #[AsEventListener('my-extension/setup-module-listener')]
    public function __invoke(AddJavaScriptModulesEvent $event): void
    {
        $event->addJavaScriptModule('@my-extension/setupModule/some-file.js');
    }
}
Copied!

For compatibility with TYPO3 v14 and v15 

Only listen to the new event \TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent :

EXT:my_extension/Classes/Listener/SetupModuleListener.php
<?php
declare(strict_types=1);

namespace MyExtension\Listener;

use TYPO3\CMS\Backend\Event\AddUserSettingsJavaScriptModulesEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class SetupModuleListener
{
    #[AsEventListener('my-extension/setup-module-listener')]
    public function __invoke(AddUserSettingsJavaScriptModulesEvent $event): void
    {
        $event->addJavaScriptModule('@my-extension/setupModule/some-file.js');
    }
}
Copied!

Important: #109585 - Serialized Credential Data in be_users settings 

See forge#109585

Description 

The new mechanism of using serialized JSON data for storing backend user settings since TYPO3 14.2 has introduced a vulnerability that stored the "password" and "verify password" input data when changing a user's password inside the serialized user settings representation.

These passwords are no longer stored in the database columns be_users.uc and be_users.user_settings anymore, but may exist in database records during the period where TYPO3 v14.2 was used.

An upgrade wizard has been added that will remove these credentials from the serialized representation.

This upgrade wizard will detect possible records that contain the string "password or :"password and then unserialize the data, remove the two fields and re-serialize the data. It is important to execute this wizard for safety. If the wizard does not show up, no serialized credential data is found.

14.2 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Feature: #32051 - Extbase query expression builder for orderings 

See forge#32051

Description 

Extbase queries now support SQL expressions in ORDER BY clauses through a new fluent API. This enables results to be sorted using functions such as CONCAT , TRIM , and COALESCE .

The following methods have been added to the \TYPO3\CMS\Extbase\Persistence\QueryInterface :

  • orderBy() - Sets a single ordering and replaces any existing orderings
  • addOrderBy() - Adds an ordering to the existing orderings
  • concat() - Creates a CONCAT expression
  • trim() - Creates a TRIM expression
  • coalesce() - Creates a COALESCE expression

Examples 

Order by concatenated fields:

$query = $this->myRepository->createQuery();
$query->orderBy(
    $query->concat('firstName', 'lastName'),
    QueryInterface::ORDER_ASCENDING
);
Copied!

Order by a trimmed field:

$query->orderBy(
    $query->trim('title'),
    QueryInterface::ORDER_DESCENDING
);
Copied!

Order by the first non-null value (a fallback pattern):

$query->orderBy(
    $query->coalesce('nickname', 'firstName'),
    QueryInterface::ORDER_ASCENDING
);
Copied!

Chain multiple orderings:

$query
    ->orderBy($query->concat('firstName', 'lastName'))
    ->addOrderBy('createdAt', QueryInterface::ORDER_DESCENDING);
Copied!

Nest expressions:

$query->orderBy(
    $query->concat(
        $query->trim('firstName'),
        $query->trim('lastName')
    )
);
Copied!

Backwards compatibility 

The existing setOrderings() method with its array syntax will continue to work:

$query->setOrderings([
    'title' => QueryInterface::ORDER_ASCENDING,
    'date' => QueryInterface::ORDER_DESCENDING,
]);
Copied!

Impact 

Developers can now use SQL functions in Extbase query orderings without resorting to raw SQL statements. This enables more flexible sorting logic while maintaining the abstraction and security benefits of the Extbase persistence layer.

Feature: #69190 - Add password generator "wizard" 

See forge#69190

Description 

Password generation in the backend is now driven by password policies. Each password policy can define a generator section through a class implementing \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface .

The passwordGenerator field control references a policy by name via the passwordPolicy option. The dice icon button next to the field generates a password using the configured generator.

The field control can be added to any password field via TCA configuration, making it available to extension developers as well.

Password policies 

Which policy to use is determined by context:

  • Backend users: $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy']
  • Frontend users: $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy']

All password policies are registered under $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .

TYPO3 ships with three preconfigured policies:

  • default — Used for backend and frontend users
  • installTool — Used for Install Tool passwords
  • secretToken — Used for secret token fields (e.g. webhooks, reactions)

Each policy contains both a generator and a validators section. The generator is responsible for creating passwords, while validators enforce password requirements. They are configured independently within the same policy.

Example 

TYPO3 ships with a PasswordGenerator implementation that is configured like this:

<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator'] = [
    'className' => \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGenerator::class,
    'options' => [
        'length' => 12,
        'upperCaseCharacters' => true,
        'lowerCaseCharacters' => true,
        'digitCharacters' => true,
        'specialCharacters' => true,
    ],
];
Copied!

The PasswordGenerator supports the following options:

  • length: Length of the generated password
  • upperCaseCharacters: Whether to include uppercase characters
  • lowerCaseCharacters: Whether to include lowercase characters
  • digitCharacters: Whether to include digits
  • specialCharacters: Whether to include special characters

Adjusting an existing policy:

config/system/additional.php OR typo3conf/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator']['options']['length'] = 20;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator']['options']['specialCharacters'] = false;
Copied!

Registering a custom password policy with a custom generator:

config/system/additional.php OR typo3conf/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['customPolicy'] = [
    'generator' => [
        'className' => \Vendor\MyPackage\PasswordPolicy\Generator\MyPasswordGenerator::class,
        'options' => [
            'length' => 12,
            'myCustomOption' => 'my custom value',
        ],
    ],
    'validators' => [
        // Your custom validators
    ],
];

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = 'customPolicy';
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = 'customPolicy';
Copied!

Impact 

Password generation for backend and frontend users is now configurable through password policies. The Install Tool command vendor/bin/typo3 install:password:set also respects the configured policy.

Feature: #78412 - Provide static TSconfig includes for be_users & be_groups 

See forge#78412

Description 

The tables be_users and be_groups are each extended by an additional field that allows static TSconfig to be selected that is defined by extensions. These fields follow the syntax of the tsconfig_includes field in the pages table. The following methods are available:

For backend users:

EXT:my_extension/Configuration/TCA/Overrides/be_users.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::registerUserTSConfigFile(
    'extensionKey',
    'Configuration/Tsconfig/Static/example1.tsconfig',
    'Example 1'
);
Copied!

For backend user groups:

EXT:my_extension/Configuration/TCA/Overrides/be_groups.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::registerUserGroupTSConfigFile(
    'extensionKey',
    'Configuration/Tsconfig/Static/example2.tsconfig',
    'Example 2'
);
Copied!

Impact 

The new fields can be used to define user TSconfig for specific users and user groups provided by extensions.

Using this approach, instead of writing TSconfig directly to the database field of be_users or be_groups, reduces the amount of configuration stored in the database. This has several advantages:

  • Running a TYPO3 instance with automated deployment and Git version control makes it easy to create or modify such an includable TSconfig snippet via a file change. It also helps keep configuration streamlined for multiple environments such as staging or production. Once the file is included, you can have the same user or group configuration without having to manually change the database for each affected user or group.
  • Extension authors can ship predefined user TSconfig files that can be included by TYPO3 backend users. This also applies to local (site) packages or your agency's base package.
  • Possible breaking changes in major upgrades can be handled automatically with tools like TYPO3 Fractor (an enhancement for TYPO3 Rector). If a breaking change occurs within user TSconfig, such a tool can automatically upgrade the configuration, ensuring that you do not miss occurrences in the database. This approach can reduce recurring manual work, especially in large TYPO3 instances with many be_users or be_groups records.
  • Searching through user TSconfig stored in the database is now possible in your IDE, as all TSconfig resides in your codebase. This can improve productivity and simplify major upgrades. You can also go further and disable or hide the TSconfig database field in projects to prevent saving user TSconfig to the database for users or user groups.

Feature: #87435 - Make new content element wizard items' sort order configurable 

See forge#87435

Description 

It is now possible to set the order of content elements inside the wizard tabs in the new content element wizard by setting before and after values in Page TSconfig.

Previously, the order of tabs could be configured but not the order of individual content elements inside a tab. This feature extends the existing ordering mechanism to individual content elements, following the same pattern as tab ordering.

This eliminates the need for workarounds, such as creating custom wizard groups to reorder elements.

Example 

mod.wizards.newContentElement.wizardItems {
    default.elements {
        textmedia {
            after = header
        }
        mask_article_card {
            after = textmedia
        }
        # Multiple elements can be specified (comma-separated)
        mask_article_list {
            after = header,textmedia
        }
        # Or use before
        header {
            before = textmedia
        }
    }
}
Copied!

Impact 

Integrators and developers can now set the exact order of content elements inside each wizard tab using Page TSconfig, without needing to create custom wizard groups. The ordering mechanism follows the same syntax and behavior as the existing tab ordering feature (forge#71876), ensuring consistency and familiarity.

This feature works alongside existing tab ordering: tabs can be ordered using before and after at the group level, and content elements inside each tab can be ordered using before and after at the element level.

If before and after configuration is not specified for elements, they retain their default order, typically the order defined in TCA.

Feature: #88470 - Custom message in form email finisher 

See forge#88470

Description 

The email finishers EmailToSender and EmailToReceiver in the TYPO3 form framework now support an optional custom message field. This allows editors to add personalized text to the email sent by the form, either before or after the submitted form values.

The message field supports rich text editing using the form-content RTE preset, which provides formatting options such as bold, italic, links, and lists.

A special placeholder {formValues} can be used inside the message to control where the submitted form data table is rendered. If the placeholder is omitted, only the custom message is shown and the form values table is hidden.

The message field is available in:

  • The form editor backend module
  • The form finisher override settings

Impact 

Editors can now configure a custom message for email finishers directly in the form editor or via finisher overrides in the form plugin. This provides more flexibility in crafting email notifications without the need for custom Fluid templates.

Feature: #89334 - Add generic TranslatorInterface 

See forge#89334

Description 

A new \TYPO3\CMS\Core\Localization\TranslatorInterface has been introduced to provide a clean abstraction for translating labels in TYPO3.

The interface defines two methods:

  • translate() translates a label using its identifier and domain, supporting argument interpolation, a default fallback value, and per-call locale overrides.
  • label() resolves full TYPO3 label reference strings, for example LLL:EXT:core/Resources/Private/Language/locallang.xlf:my.key, and delegates to translate() . This method serves as the interface-based equivalent of LanguageService::sL() . The key differences are that it returns null when a label cannot be resolved instead of an empty string, and supports argument interpolation, default values, and locale overrides.

LanguageService now implements this interface, making it possible to type-hint against the interface instead of the concrete class.

Example usage:

Using TranslatorInterface via dependency injection
use TYPO3\CMS\Core\Localization\TranslatorInterface;

final class MyController
{
    public function __construct(
        private readonly TranslatorInterface $translator,
    ) {}

    public function someAction(): void
    {
        // Translate by identifier and domain
        $label = $this->translator->translate(
            'button.save',
            'my_extension.messages',
        );

        // Translate by identifier and filename (discouraged)
        $label = $this->translator->translate(
            'button.save',
            'EXT:my_extension/Resources/Private/Language/locallang.xlf',
        );

        // Translate with arguments
        $label = $this->translator->translate(
            'record.count',
            'my_extension.messages',
            [5],
        );

        // Translate with a default fallback
        $label = $this->translator->translate(
            'missing.key',
            'my_extension.messages',
            [],
            'Fallback text',
        );

        // Label reference with arguments and default
        $label = $this->translator->label(
            'my_extension.messages:record.count',
            [5],
            'Fallback text',
        );

        // Resolve a full label reference string with file and LLL prefix (discouraged)
        $label = $this->translator->label(
            'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:button.save',
        );
    }
}
Copied!

The label() method accepts the following reference formats:

  • my_extension.messages:my.key
  • EXT:my_extension/Resources/Private/Language/locallang.xlf:my.key
  • LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my.key

The LLL: prefix is optional and removed before resolution.

Impact 

Extension developers can now type-hint against TranslatorInterface instead of the concrete LanguageService class. This improves testability and decouples code from implementation.

The translate() method of LanguageService has been extended with two additional optional parameters:

  • $default a fallback value returned when the label is not found
  • $locale allows overriding the locale on a per-call basis

Feature: #89951 - Cleanup command for form file upload folders 

See forge#89951

Description 

A new CLI command form:cleanup:uploads has been introduced to clean up old file upload folders created by the TYPO3 form framework.

When users upload files via FileUpload or ImageUpload form elements, the files are stored in form_<hash> subfolders inside the upload directory. Over time these folders accumulate due to complete and incomplete form submissions.

Since uploaded files are not moved upon form submission, there is no way to distinguish between folders from completed and abandoned submissions. The command identifies form upload folders by their naming pattern, form_ followed by exactly 40 hexadecimal characters, and their modification time. Folders older than a configurable retention period, by default 2 weeks, can be removed.

You must specify at least one upload folder to scan. Since each form element can configure a different upload folder via the saveToFileMount property, for example 1:/user_upload/ or 2:/custom_uploads/ , pass all relevant folders as arguments.

Usage 

# Dry run: list form upload folders older than 2 weeks (default)
bin/typo3 form:cleanup:uploads 1:/user_upload/ --dry-run

# Delete folders older than 48 hours
bin/typo3 form:cleanup:uploads 1:/user_upload/ --retention-period=48

# Scan multiple upload folders
bin/typo3 form:cleanup:uploads 1:/user_upload/ 2:/custom_uploads/

# Force deletion without confirmation (useful for scheduler tasks)
bin/typo3 form:cleanup:uploads 1:/user_upload/ --force

# Verbose output shows details about each folder
bin/typo3 form:cleanup:uploads 1:/user_upload/ --dry-run -v
Copied!

Arguments 

upload-folder
Combined folder identifier or identifiers to scan (required). Multiple folders can be specified as separate arguments.

Options 

--retention-period / -r
Minimum time in hours before a folder is considered for removal (default: 336, that is, 2 weeks).
--dry-run
List expired folders without deleting them.
--force / -f
Skip the interactive confirmation prompt. This is automatically set with --no-interaction , for example, in the TYPO3 Scheduler.

Scheduler integration 

The command is available as a Scheduler task since it is registered via the #[AsCommand] attribute. Configure it to run periodically, for example, once a day or once a week, to keep the upload folders clean.

Impact 

The new command provides a safe and configurable way to reclaim disk space from accumulated form upload folders. The conservative default retention period of 2 weeks ensures that files belonging to forms still being actively worked on are not accidentally removed.

Feature: #91724 - Introduce TemplatedEmailFactory for centralized email creation 

See forge#91724

Description 

A new TemplatedEmailFactory class has been introduced to provide a centralized creation of FluidEmail instances.

The factory provides three methods for different use cases:

create()
For backend and CLI contexts, such as login notifications, Scheduler tasks, and the Install Tool, where only the global configuration $GLOBALS['TYPO3_CONF_VARS']['MAIL'][...] is used.
createFromRequest()
For frontend contexts, such as form submissions and EXT:felogin, where site-specific email templates should be applied. It merges site settings from typo3/email with the global configuration $GLOBALS['TYPO3_CONF_VARS']['MAIL'][...] .
createWithOverrides()

For extensions that need to provide custom template paths merged on top of the base configuration, optionally taking a request context into account. Two cases of template resolution are possible, ordered by priority:

  • Request without site attribute: 1. Provided override arguments -> 2. global configuration
  • Request with site attribute: 1. Provided override arguments -> 2. site settings -> 3. global configuration

Note that you can also use the numerical priority of template paths so that site settings with a higher priority number can override paths in the provided arguments with a lower priority number.

Site settings 

A new site set, typo3/email , is available in EXT:core and defines the settings below. These are applied automatically when a request with a site attribute is passed to createFromRequest() or createWithOverrides() . This means extensions running in a frontend context, such as EXT:form email finishers, benefit from site-specific email configuration:

email.format
The email format to use (html, plain, both). If empty, the global configuration is used.
email.templateRootPaths
An array of paths to email templates. These are merged with the global mail template paths.
email.layoutRootPaths
An array of paths to email layouts. These are merged with the global mail layout paths.
email.partialRootPaths
An array of paths to email partials. These are merged with the global mail partial paths.

Usage 

Frontend usage (site-aware) 

For frontend contexts where site-specific templates are desired, use createFromRequest() . Include the typo3/email site set in your site configuration:

config/sites/my-site/config.yaml
dependencies:
  - typo3/email

settings:
  email:
    templateRootPaths:
      100: 'EXT:my_sitepackage/Resources/Private/Templates/Email/'
    layoutRootPaths:
      100: 'EXT:my_sitepackage/Resources/Private/Layouts/Email/'
    format: 'html'
Copied!
EXT:my_extension/Classes/Service/MyFrontendEmailService.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;

final class MyFrontendEmailService
{
    public function __construct(
        private readonly TemplatedEmailFactory $templatedEmailFactory,
        private readonly MailerInterface $mailer,
    ) {}

    public function sendEmail(ServerRequestInterface $request): void
    {
        // Uses site-specific template paths if configured
        $email = $this->templatedEmailFactory->createFromRequest($request);
        $email
            ->setTemplate('MyTemplate')
            ->to('recipient@example.com')
            ->from('sender@example.com')
            ->subject('My Subject')
            ->assign('name', 'World');

        $this->mailer->send($email);
    }
}
Copied!

Backend and CLI usage 

For backend contexts where no site-specific templates are needed, use create() :

EXT:my_extension/Classes/Service/MyBackendEmailService.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;

final class MyBackendEmailService
{
    public function __construct(
        private readonly TemplatedEmailFactory $templatedEmailFactory,
        private readonly MailerInterface $mailer,
    ) {}

    public function sendNotification(
        ?ServerRequestInterface $request = null,
    ): void {
        // Uses only global $GLOBALS['TYPO3_CONF_VARS']['MAIL'] configuration
        $email = $this->templatedEmailFactory->create($request);
        $email
            ->setTemplate('SystemNotification')
            ->to('admin@example.com')
            ->from('system@example.com')
            ->subject('System Notification');

        $this->mailer->send($email);
    }
}
Copied!

Custom template path overrides 

For extensions that need their own email templates merged with the global configuration, use createWithOverrides() :

EXT:my_extension/Classes/Task/MySchedulerTask.php
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

final class MySchedulerTask extends AbstractTask
{
    public function sendReport(): void
    {
        // This example shows how to use this when constructor-based
        // dependency injection is not possible, as in AbstractTask
        // (EXT:scheduler). Always use dependency injection where possible.
        $mailer = GeneralUtility::makeInstance(MailerInterface::class);
        $templatedEmailFactory = GeneralUtility::makeInstance(
            TemplatedEmailFactory::class
        );

        // Merge extension-specific paths with the global configuration.
        // Note that if you do not pass a `$request` argument here, no site
        // context is evaluated. You may want to check
        // `$GLOBALS['TYPO3_REQUEST']` if you need this fallback, or use a
        // custom request object.
        $email = $templatedEmailFactory->createWithOverrides(
            templateRootPaths: [
                20 => 'EXT:my_extension/Resources/Private/Templates/Email/',
            ],
            layoutRootPaths: [
                20 => 'EXT:my_extension/Resources/Private/Layouts/',
            ],
        );
        $email
            ->setTemplate('Report')
            ->to('admin@example.com')
            ->from('system@example.com')
            ->subject('Scheduled Report');

        $mailer->send($email);
    }
}
Copied!

Core migrations 

All core extensions that send emails have been migrated to use TemplatedEmailFactory . This includes:

  • EXT:form Email finishers now use createWithOverrides() with the request from the form runtime, so site-specific email settings are applied automatically.
  • EXT:felogin Password recovery emails now use createWithOverrides() , making them site-aware. The method RecoveryConfiguration::getMailTemplatePaths() has been removed, as template path resolution is now handled by the factory.
  • EXT:backend Login notifications, failed login and MFA attempt notifications, and password reset emails use create() .
  • EXT:install Test email sending uses create() .
  • EXT:workspaces Stage change notifications use createWithOverrides() .
  • EXT:linkvalidator Broken link report emails use createWithOverrides() .
  • EXT:reports System status emails use create() .

Impact 

Extensions that send emails are encouraged to use the TemplatedEmailFactory to create FluidEmail instances instead of instantiating them directly. When a request with a site attribute is passed, template paths and format from the typo3/email site set are applied. The merge priority, with the highest priority winning, is:

  1. Global $GLOBALS['TYPO3_CONF_VARS']['MAIL'] paths as the base
  2. Site settings from typo3/email , when a site-based request is available and site settings are applied
  3. Caller-provided override paths when using createWithOverrides()

Feature: #91924 - Add form element selection buttons to property grid 

See forge#91924

Description 

A form element selection button has been added to the property grid editors of the EmailToSender and EmailToRecipient finishers.

Impact 

A button is now displayed in the property grid that allows direct selection of form element identifiers without requiring manual input.

Feature: #92780 - Introduce event after page URI generation 

See forge#92780

Description 

A new PSR-14 event \TYPO3\CMS\Core\Routing\Event\AfterPageUriGeneratedEvent is dispatched in \TYPO3\CMS\Core\Routing\PageRouter::generateUri() .

The event provides access to the generated URI and the arguments passed to generateUri() . Listeners can inspect and replace the generated URI. The parameters payload reflects the sanitized query arguments after handling special parameters such as id and _language .

The event has the following methods:

  • getUri() and setUri()
  • getRoute()
  • getParameters()
  • getFragment()
  • getType()
  • getLanguage()
  • getSite()

When replacing the URI, listeners must ensure that the returned URI is valid in their setup and remains routable.

Example listener registration:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Routing\Event\AfterPageUriGeneratedEvent;
use TYPO3\CMS\Core\Routing\RouterInterface;

#[AsEventListener('my-extension/after-page-uri-generated')]
final readonly class MyListener
{
    public function __invoke(AfterPageUriGeneratedEvent $event): void
    {
        // Only act on absolute URLs
        if ($event->getType() !== RouterInterface::ABSOLUTE_URL) {
            return;
        }

        // Inspect or replace $event->getUri()
    }
}
Copied!

Impact 

Extension authors can now react to generated page URIs for use cases such as logging, monitoring, debugging, and URL adjustment. Because the event is dispatched in all contexts that use PageRouter::generateUri() , including the backend and various subsystems, listeners should scope their modifications carefully.

Feature: #95910 - Add a language selector in the record history/undo view 

See forge#95910

Description 

The backend view for listing the history or audit trail of a record and for undo or rollback functionality has been enhanced by adding a language selector to give editors the ability to switch between different translations of a record.

The new language selection dropdown is shown only if the record is language- aware. Available languages are determined by the translations and a user's allowed_languages as defined by their groups.

Impact 

The history/undo view of a translated record can now be accessed from the page tree and other places where a context menu is available, not just from the Content > Records module and the Content > Layout module language comparison view.

Feature: #97637 - Translate default value of form elements 

See forge#97637

Description 

The TYPO3 form framework now supports translating the defaultValue property of form elements via XLF translation files.

Previously only properties such as label , placeholder , and rendering options could be translated through the form framework's translation mechanism. The defaultValue property was always rendered as-is from the form definition, regardless of the current frontend language.

Now, defaultValue is translated before rendering using the same XLF key conventions already established for other form element properties. The translation is resolved in the following order, with the first match winning:

  1. <form-definition-identifier>.element.<element-identifier>.properties.defaultValue
  2. element.<element-identifier>.properties.defaultValue
  3. element.<element-type>.properties.defaultValue

Example 

Given a form element defined as follows:

fileadmin/form_definitions/contact.form.yaml
identifier: contact-form
type: Form
prototypeName: standard
renderables:
  - type: Page
    identifier: page-1
    renderables:
      - type: Text
        identifier: bestDish
        label: 'Best dish?'
        defaultValue: 'Hamburger'
        renderingOptions:
          translation:
            translationFiles:
              - 'EXT:my_extension/Resources/Private/Language/Form/locallang.xlf'
Copied!

The translation file can now provide a translated default value:

EXT:my_extension/Resources/Private/Language/Form/de.locallang.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" target-language="de" datatype="plaintext" original="messages">
        <body>
            <trans-unit id="contact-form.element.bestDish.properties.defaultValue">
                <source>Hamburger</source>
                <target>Kartoffel</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

Or, to apply the translation to all Text elements across all forms:

EXT:my_extension/Resources/Private/Language/Form/de.locallang.xlf
<trans-unit id="element.Text.properties.defaultValue">
    <source>Hamburger</source>
    <target>Kartoffel</target>
</trans-unit>
Copied!

Impact 

Form integrators can now provide language-specific default values for form elements. The translation is applied automatically before rendering using the existing translation file configuration in renderingOptions.translation.translationFiles .

Array-based defaultValue properties are intentionally excluded from translation. These occur in multi-value elements such as MultiCheckbox or MultiSelect , where the values are option keys that must match the configured properties.options exactly. Translating them would break the value-to-option mapping. The option labels themselves can be translated via the existing properties.options.[*] translation mechanism.

Feature: #97898 - TCA option isViewable for page types 

See forge#97898

Description 

A new TCA option isViewable is introduced for page types (doktype) to configure whether a specific page type can be linked to in the page browser and in frontend TypoLink generation.

EXT:my_extension/Configuration/TCA/Overrides/pages.php
// Disable linking for custom page type
$GLOBALS['TCA']['pages']['types']['116']['isViewable'] = false;
Copied!

By default, all page types are viewable unless explicitly set to false .

TYPO3 core now marks the following page types as non-viewable in TCA:

  • Spacer (doktype 199)
  • SysFolder (doktype 254)

The existing TSconfig option TCEMAIN.preview.disableButtonForDokType is also respected when determining viewability in the backend page browser. If a page type is disabled for preview via TSconfig, it is also non-viewable.

Impact 

The viewability of pages can now be configured in TCA, following the same pattern as the allowedRecordTypes option introduced in TYPO3 v14.1. This provides a centralized way to control which page types can be linked.

Extensions with custom page types that should not be viewable can now configure this directly in TCA:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types'][(string)\MyVendor\MyExtension\Domain\PageType::MY_NON_VIEWABLE_TYPE] = [
    'isViewable' => false,
    'showitem' => '...',
];
Copied!

Feature: #99491 - PSR-14 event for redirect integrity checks 

See forge#99491

Description 

A new PSR-14 event \TYPO3\CMS\Redirects\Event\RedirectIntegrityCheckEvent has been added. It is dispatched in \TYPO3\CMS\Redirects\Service\IntegrityService->checkRedirectTargetIntegrity() for each redirect record.

While the existing integrity check verifies only whether redirect sources conflict with page URLs (self-reference), this event allows extensions to validate redirects for other conflict types. For example, an extension can check whether a t3://record link target still resolves correctly or whether an external URL returns a valid response.

The event provides the following methods:

  • getRedirect() : The full sys_redirect record as an array.
  • getUid() : Convenience method returning the redirect uid as an integer.
  • getPid() : Convenience method returning the redirect pid as an integer.
  • getDeleted() : Convenience method returning the redirect deleted as a boolean.
  • getDisabled() : Convenience method returning the redirect disabled as a boolean.
  • getSourceHost() : Convenience method returning the redirect source_host as a string.
  • getSourcePath() : Convenience method returning the redirect source_path as a string.
  • getIsRegExp() : Convenience method returning the redirect is_regexp as a boolean.
  • getProtected() : Convenience method returning the redirect protected as a boolean.
  • getForceHttps() : Convenience method returning the redirect force_https as a boolean.
  • getRespectQueryParameters() : Convenience method returning the redirect respect_query_parameters as a boolean.
  • getKeepQueryParameters() : Convenience method returning the redirect keep_query_parameters as a boolean.
  • getTarget() : Convenience method returning the redirect target as a string.
  • getTargetStatusCode() : Convenience method returning the redirect target_statuscode as an integer.
  • getCreationType() : Convenience method returning the redirect creation_type as an integer.
  • getOriginalIntegrityStatus() : Convenience method returning the redirect integrity_status as a string.
  • getIntegrityStatus() / setIntegrityStatus() : Read or set the integrity status. When a listener sets a non-null status, the redirect is reported as a conflict in the redirects:checkintegrity command output. Be aware that \TYPO3\CMS\Redirects\Utility\RedirectConflict::NO_CONFLICT can be set as the integrity status and will not be included in the report, even if a listener explicitly sets \TYPO3\CMS\Redirects\Utility\RedirectConflict::NO_CONFLICT for the redirect.

Additionally, the following new class constant has been added to allow extensions to conveniently reuse a shared conflict status in custom event listeners:

  • \TYPO3\CMS\Redirects\Utility\RedirectConflict::INVALID_TARGET

Example 

An event listener that validates t3://record targets:

EXT:my_extension/Classes/EventListener/ValidateRedirectTarget.php
<?php

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Redirects\Event\RedirectIntegrityCheckEvent;
use TYPO3\CMS\Redirects\Utility\RedirectConflict;

final readonly class ValidateRedirectTarget
{
    public function __construct(
        private ConnectionPool $connectionPool,
    ) {}

    #[AsEventListener('my-extension/validate-redirect-target')]
    public function __invoke(RedirectIntegrityCheckEvent $event): void
    {
        $target = $event->getTarget();
        if (!str_starts_with($target, 't3://record')) {
            return;
        }
        // Parse t3://record?identifier=tx_news&uid=456
        parse_str((string)parse_url($target, PHP_URL_QUERY), $params);
        $table = $params['identifier'] ?? '';
        $uid = (int)($params['uid'] ?? 0);
        if ($table === '' || $uid === 0) {
            $event->setIntegrityStatus(RedirectConflict::INVALID_TARGET);
            return;
        }
        $count = $this->connectionPool
            ->getConnectionForTable($table)
            ->count('uid', $table, ['uid' => $uid]);
        if ($count === 0) {
            $event->setIntegrityStatus(RedirectConflict::INVALID_TARGET);
            return;
        }
        // Set to NO_CONFLICT. This will not be reported as a conflicting
        // redirect, but it clears any previously set integrity status.
        $event->setIntegrityStatus(RedirectConflict::NO_CONFLICT);
    }
}
Copied!

Impact 

Extensions can now validate redirects during the integrity check by listening to this event. Broken or invalid redirects are reported alongside existing self-reference conflicts in the redirects:checkintegrity command output.

Feature: #100887 - Prefer CSP hash values over nonce values 

See forge#100887

Description 

Content-Security-Policy nonce values are random tokens in each request that prevent HTTP response caching. By collecting hash values of assets at render time instead, responses can be cached, for example by using lochmueller/staticfilecache or reverse proxies, while still enforcing a strict CSP.

Hash-based CSP is an explicit opt-in configured for a site via csp.yaml. Nonce values remain the default when no behavior is configured.

New DirectiveHashCollection service 

The new \TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection service is a per-request registry that collects CSP hash values for inline and static assets during page rendering.

Both inline content and static file resources are supported:

  • Inline assets: the SHA-256 hash is computed over the content that appears inside the <script> or <style> element.
  • Static assets: if an integrity attribute is already present, its value is reused; otherwise, the file content is hashed on demand.
  • Style attributes: the new f:asset.styleAttr ViewHelper hashes inline style values and covers the style-src-attr directive.

The collected hashes survive the frontend page cache round-trip via \TYPO3\CMS\Frontend\Cache\MetaDataState .

Updated Behavior class 

\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior now carries a second nullable boolean property, $useHash :

  • true explicitly enables hash collection and CSP hash sources.
  • null means off, which is the default. Hashes are not collected or applied.
  • false explicitly disables hash collection and CSP hash sources.

Configuring behavior via csp.yaml 

Both $useNonce and $useHash can be set in a site's config/sites/<site>/csp.yaml under the top-level behavior: key:

behavior:
  useNonce: false
  useHash: true

enforce:
  inheritDefault: true
  includeResolutions: true
Copied!

Setting useHash: true enables hash-based CSP for that site. Setting useNonce: false removes nonce sources from the compiled policy, which is required for responses to be cacheable by reverse proxies.

Updated Policy::prepare() and Policy::compile() 

Both methods now accept a \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag instead of separate ConsumableNonce , Behavior , and HashCollection arguments. The PolicyBag is forwarded directly from the CSP middleware, making hash collection visible to PSR-14 event listeners via PolicyPreparedEvent::$policyBag->directiveHashCollection .

The behavior resolution, applying collected hashes and suppressing nonce sources, now happens inside Policy::prepare() .

New f:asset.styleAttr ViewHelper 

A new ViewHelper registers inline style values using the style-src-attr CSP directive:

<div style="{f:asset.styleAttr(value: 'color: green', csp: true)}"></div>
Copied!

The csp argument defaults to true and controls whether the hash is collected.

Updated f:asset.script and f:asset.css ViewHelpers 

The useNonce argument has been renamed to csp (deprecated, see Deprecation: #100887 - Deprecation of useNonce argument in f:asset:css and f:asset:script view helpers). The new default is true for external files, that is, static resources, and false for inline content.

<!-- static file: csp=1 by default, hash collected from file content -->
<f:asset.script
    identifier="my-script"
    src="EXT:my_ext/Resources/Public/JavaScript/foo.js"
/>

<!-- with integrity attribute: hash reused directly, no file read -->
<f:asset.script
    identifier="my-script"
    src="EXT:my_ext/Resources/Public/JavaScript/foo.js"
    integrity="sha256-abc123=="
/>

<!-- inline script: opt in explicitly -->
<f:asset.script identifier="my-inline" csp="1">
    document.querySelector('.foo').classList.add('active');
</f:asset.script>
Copied!

Migration 

The useNonce ViewHelper argument and 'useNonce' asset option key are deprecated and replaced by csp and 'csp' . See Deprecation: #100887 - Deprecation of useNonce argument in f:asset:css and f:asset:script view helpers.

The signature of Policy::prepare() and Policy::compile() has changed to accept a PolicyBag . Code calling these methods directly, as they are marked @internal , must be updated.

Impact 

Sites that configure behavior.useHash: true , and optionally behavior.useNonce: false , in their csp.yaml can use hash-based CSP sources. This allows HTTP responses to be cached by reverse proxies and static file cache extensions without sacrificing Content-Security-Policy enforcement. Sites without this configuration continue to use nonce-based CSP.

Feature: #102079 - Introduce BeforePersistingReportEvent for CSP violations 

See forge#102079

Description 

When a Content-Security-Policy violation report needs to be persisted, the \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\BeforePersistingReportEvent can be used to provide an alternative report or to prevent a particular report from being persisted.

Example 

<?php
declare(strict_types=1);

namespace Example\Demo\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\BeforePersistingReportEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;

final class BeforePersistingReportEventListener
{
    private const BROWSER_PREFIXES = [
        'chrome-extension://',
        'moz-extension://',
        'safari-extension://',
    ];

    #[AsEventListener('example/security/before-persisting-csp-report')]
    public function __invoke(BeforePersistingReportEvent $event): void
    {
        // Avoid persisting CSP violations caused by browser extensions
        $blockedUri = $event->originalReport->details['blocked-uri'] ?? null;
        if (is_string($blockedUri) && $this->isBrowserExtensions($blockedUri)) {
            $event->report = null;
            return;
        }

        // Otherwise, adjust the report and provide custom metadata
        $event->report = new Report(
            $event->originalReport->scope,
            $event->originalReport->status,
            $event->originalReport->requestTime,
            array_merge(
                $event->originalReport->meta,
                ['x-example' => '... additional metadata ...']
            ),
            $event->originalReport->details,
            $event->originalReport->summary,
            $event->originalReport->uuid,
            $event->originalReport->created,
            $event->originalReport->changed
        );
    }

    private function isBrowserExtensions(string $blockedUri): bool
    {
        foreach (self::BROWSER_PREFIXES as $prefix) {
            if (str_starts_with($blockedUri, $prefix)) {
                return true;
            }
        }

        return false;
    }
}
Copied!

Impact 

The new BeforePersistingReportEvent allows custom control over whether and how Content-Security-Policy violation reports are persisted in TYPO3.

Feature: #102159 - Support additional parameters for TCA slug prefix userFunc 

See forge#102159

Description 

TCA slug prefix user functions now receive the full TCA field configuration and the field name as additional parameters.

User function implementation 

The prefix user function receives two additional keys alongside the existing parameters:

  • fieldName - The name of the slug field
  • config - The full TCA configuration array of the slug field
EXT:my_extension/Classes/Utility/SlugUtility.php
namespace MyExtension\Utility;

class SlugUtility
{
    public function generatePrefix(array $parameters): string
    {
        // Standard parameters (always available)
        $site = $parameters['site'];
        $languageId = $parameters['languageId'];
        $table = $parameters['table'];
        $row = $parameters['row'];

        $fieldName = $parameters['fieldName'];
        $config = $parameters['config'];

        return '/default/';
    }
}
Copied!

Available parameters 

The user function receives an array with the following keys:

  • site - The current site object
  • languageId - The current language ID (int)
  • table - The table name (string)
  • row - The current record data (array)
  • fieldName - The name of the slug field (string)
  • config - The full TCA configuration array of the slug field

Impact 

Extension developers can access the complete TCA field configuration and the field name inside prefix user functions.

Feature: #102194 - Introduce QueryBuilderPaginator 

See forge#102194

Description 

A new \TYPO3\CMS\Core\Pagination\QueryBuilderPaginator is introduced to enable pagination of QueryBuilder instances.

The paginator implements the existing PaginatorInterface and integrates seamlessly with the existing SimplePagination and SlidingWindowPagination classes.

The paginated items are fetched only once per page request by storing the result internally, avoiding double execution of the database statement.

The total item count is determined robustly using a common table expression (CTE) wrapping the passed QueryBuilder instance. This approach correctly handles advanced queries involving UNION, nested CTEs, window functions, and grouping.

Impact 

A new QueryBuilderPaginator is available to paginate QueryBuilder result sets using the TYPO3 pagination API.

Example 

EXT:my_extension/Classes/Controller/MyController.php
use TYPO3\CMS\Core\Pagination\QueryBuilderPaginator;
use TYPO3\CMS\Core\Pagination\SimplePagination;

$paginator = new QueryBuilderPaginator(
    queryBuilder: $queryBuilder,
    currentPageNumber: $currentPage,
    itemsPerPage: 10,
);
$pagination = new SimplePagination($paginator);

// Retrieve the items for the current page
$items = $paginator->getPaginatedItems();
Copied!

Feature: #102215 - ViewHelper and data structure to render srcset attribute 

See forge#102215

Description 

The srcset HTML attribute can be used to provide different image sizes to the browser. The browser is free to choose which image size to use, which is why the images must all be scaled versions of the same original image. Each image in the srcset list also has a descriptor which either specifies the absolute width of the image, for example 400w, or is a scale factor relative to the original image size for use on high-density screens, for example 2x.

srcset attributes are used by various HTML tags:

  • <img srcset="image@500.jpg 500w, image@1000.jpg 1000w" />
  • <source srcset="image@1x.jpg 1x, image@2x.jpg 2x" /> inside <picture>
  • <link rel="preload" as="image" imagesrcset="image@500.jpg 500w, image@1000.jpg 1000w" />

To generate srcset attributes easily based on input, a new data structure has been added to calculate the appropriate image sizes from a list of descriptors. Based on these calculations, image files can be generated using the image manipulation API.

EXT:my_ext/Classes/Service/SomeImageService.php
use TYPO3\CMS\Core\Html\Srcset\SrcsetAttribute;

// From width descriptors
$srcset = SrcsetAttribute::createFromDescriptors(['400w', '600w', '800w']);

// Or from pixel density descriptors (a reference width must be supplied)
$srcset = SrcsetAttribute::createFromDescriptors(
    ['1.5x', '2x', '3x'],
    800
);

// Add image URIs
foreach ($srcset->getCandidates() as $candidate) {
    // Generate scaled image here using $candidate->getCalculatedWidth()

    // Set URI of the generated image
    $candidate->setUri($generatedImageUri);
}

// Render srcset attribute
$srcsetString = $srcset->generateSrcset();
Copied!

To generate srcset attributes in Fluid templates, a new ViewHelper has also been introduced.

<picture>
    <source
        srcset="{f:image.srcset(image: imageObject, srcset: '400w, 600w, 800w', cropVariant: 'wide')}"
        sizes="100vw"
        media="(min-width: 1200px)"
    />
    <!-- ... -->
</picture>
Copied!

Impact 

The new ViewHelper f:image.srcset simplifies previous manual implementations that used f:uri.image for each image size. This now makes it easier to provide images in different dimensions based on a single image.

Feature: #102430 - Flush cache tags for file and folder operations 

See forge#102430

Description 

This feature is guarded by the frontend.cache.autoTagging feature toggle and is currently experimental. The core flushes cache tags automatically for all kinds of records when they are created, changed, or deleted. This is not the case for files and folders. This feature adds cache tag handling for file and folder operations when they are created, changed, or deleted. File metadata changes are now handled correctly as well. This will lead to a better editor experience if cache tags are used correctly.

Impact 

Integrators and extension developers can now add sys_file_${uid} and sys_file_metadata_${uid} as cache tags, and they are flushed correctly by the TYPO3 core when an editor interacts with them in the Media module.

Feature: #102790 - Line wrapping option for code editor 

See forge#102790

Description 

A new TCA appearance option lineWrapping has been added for the codeEditor render type. When enabled, long lines are wrapped inside the editor instead of requiring horizontal scrolling.

Example:

'config' => [
    'type' => 'text',
    'renderType' => 'codeEditor',
    'format' => 'html',
    'appearance' => [
        'lineWrapping' => true,
    ],
],
Copied!

Impact 

Code editor fields can now be configured to wrap long lines by setting lineWrapping in the appearance array.

Feature: #104546 - Support ICU MessageFormat for plural forms 

See forge#104546

Description 

TYPO3 now supports ICU MessageFormat for translations. This enables the proper handling of plural forms, gender-based selections, and other locale-aware formatting in language labels.

ICU MessageFormat is an internationalization standard that allows messages to contain placeholders that can vary based on parameters such as quantity, gender, or other conditions. This is particularly useful for proper pluralization in languages with complex plural rules.

The format is detected automatically when named arguments, that is, associative arrays, are used in translation calls. If the message contains ICU patterns like {count, plural, ...} or {name}, and named arguments are provided, the ICU MessageFormatter is used automatically.

Language file format 

ICU MessageFormat strings are stored as regular translation strings in XLIFF files:

EXT:my_extension/Resources/Private/Language/locallang.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="locallang.xlf">
        <body>
            <!-- Simple plural form -->
            <trans-unit id="file_count">
                <source>{count, plural, one {# file} other {# files}}</source>
            </trans-unit>

            <!-- Plural with zero case -->
            <trans-unit id="item_count">
                <source>{count, plural, =0 {no items} one {# item} other {# items}}</source>
            </trans-unit>

            <!-- Combined placeholder and plural -->
            <trans-unit id="greeting">
                <source>Hello {name}, you have {count, plural, one {# message} other {# messages}}.</source>
            </trans-unit>

            <!-- Gender selection -->
            <trans-unit id="profile_update">
                <source>{gender, select, male {He} female {She} other {They}} updated the profile.</source>
            </trans-unit>

            <!-- Simple named placeholder -->
            <trans-unit id="welcome">
                <source>Welcome, {name}!</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

PHP usage 

Use named arguments in an associative array to trigger ICU MessageFormat processing:

Using ICU MessageFormat with LanguageService
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

$languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)
    ->createFromUserPreferences($backendUser);

// ICU plural forms: use named arguments
$label = $languageService->translate(
    'file_count',
    'my_extension.messages',
    ['count' => 5]
);
// Result: "5 files"

// Combined placeholder and plural
$label = $languageService->translate(
    'greeting',
    'my_extension.messages',
    ['name' => 'John', 'count' => 3]
);
// Result: "Hello John, you have 3 messages."

// sprintf-style still works with positional arguments
$label = $languageService->translate(
    'downloaded_times',  // Label: "Downloaded %d times"
    'my_extension.messages',
    [42]  // Positional arguments use sprintf
);
// Result: "Downloaded 42 times"
Copied!
Using ICU MessageFormat with LocalizationUtility
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

// Use named arguments for ICU format
$label = LocalizationUtility::translate(
    'file_count',
    'MyExtension',
    ['count' => 1]
);
// Result: "1 file"
Copied!

Fluid usage 

In Fluid templates use named arguments in the arguments attribute:

EXT:my_extension/Resources/Private/Templates/Example.html
<!-- ICU plural forms with named arguments -->
<f:translate key="file_count" arguments="{count: numberOfFiles}" />

<!-- Combined placeholder and plural -->
<f:translate key="greeting" arguments="{name: userName, count: messageCount}" />

<!-- Gender selection -->
<f:translate key="profile_update" arguments="{gender: userGender}" />

<!-- sprintf-style with positional arguments still works -->
<f:translate key="downloaded_times" arguments="{0: downloadCount}" />
Copied!

ICU MessageFormat syntax reference 

Plural forms:

{variable, plural,
    =0 {zero case}
    one {singular case}
    other {plural case}
}
Copied!

Select (gender/choice):

{variable, select,
    male {He}
    female {She}
    other {They}
}
Copied!

Number formatting:

{count, number}           - Basic number
{price, number, currency} - Currency format
Copied!

The # symbol in plural patterns is replaced by the actual number.

Impact 

This feature provides a standards-based approach to pluralization that:

  • uses the well-tested ICU library, via PHP's intl extension
  • handles locale-specific plural rules
  • supports complex pluralization for languages such as Russian and Arabic
  • is backward compatible; existing sprintf-style translations will continue to work

The system detects which format to use based on arguments:

  • Named arguments (associative array): Uses ICU MessageFormat
  • Positional arguments (indexed array): Uses sprintf

Feature: #105084 - Add setting to configure indexed_search pagination 

See forge#105084

Description 

A new TypoScript setting plugin.tx_indexedsearch.settings.pagination_type has been introduced to set the pagination implementation used by EXT:indexed_search.

Available values:

  • simple : uses SimplePagination and renders all result pages.
  • slidingWindow : uses SlidingWindowPagination and limits the displayed page links as set in plugin.tx_indexedsearch.settings.page_links .

The default is simple to preserve existing behavior. Integrators can switch to slidingWindow to make page_links effective for indexed_search result browsing.

Impact 

Integrators can now switch between core pagination implementations using TypoScript, without having to use custom PHP code.

Advanced, fully-customized pagination logic can still be implemented using ModifySearchResultSetsEvent .

Feature: #105649 - New PSR-14 CustomFileSelectorsEvent 

See forge#105649

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Form\Event\CustomFileSelectorsEvent has been added. It is dispatched in FilesControlContainer during the rendering of selectors for relations to sys_file_references.

To modify the selectors used to add files, the following methods are available:

  • getSelectors() : Get all selectors
  • setSelectors() : Set all selectors
  • getJavascriptModules() : Get all JavaScript modules
  • setJavascriptModules() : Set all JavaScript modules
  • getTableName() : Get the table name of the current record
  • getFieldName() : Get the field name of the element
  • getDatabaseRow() : Get the raw database row
  • getFieldConfig() : Get the TCA configuration of the current field
  • getFileExtensionFilter() : Get the allowed and disallowed file extensions
  • getFormFieldIdentifier() : Get the DOM object ID used in the form

Example 

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Form\Event\CustomFileSelectorsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-extension/custom-file-selector')]
final class CustomFileSelectorEventListener
{
    public function __construct(
        private CustomDamFileSelector $damFileSelector,
    ) {}

    public function __invoke(CustomFileSelectorsEvent $event): void
    {
        $result = $this->damFileSelector->renderFileSelector(
            $event->getFormFieldIdentifier(),
        );
        $event->setSelectors(array_merge(
            $event->getSelectors(),
            $result['control'],
        ));
        $event->setJavascriptModules(array_merge(
            $event->getJavascriptModules(),
            $result['javaScriptModule'],
        ));
    }
}
Copied!

Impact 

It is now possible to modify file selectors using the new PSR-14 event CustomFileSelectorsEvent . This is especially useful for integrating a DAM system.

Feature: #105708 - Multiple file upload for EXT:form elements 

See forge#105708

Description 

The TYPO3 form framework now supports multiple file uploads in the FileUpload and ImageUpload form elements. This allows users to select and upload multiple files using a single form field.

The implementation follows the same security patterns as Extbase file upload handling. It uses HMAC-signed deletion requests to ensure secure file removal.

Configuration 

To enable multiple file uploads for a form element, set the multiple property to true in your form definition:

fileadmin/form_definitions/someForm.yaml
type: Form
identifier: contact-form
label: 'Contact Form'
prototypeName: standard
renderables:
  - type: Page
    identifier: page-1
    label: 'Page 1'
    renderables:
      - type: FileUpload
        identifier: attachments
        label: 'Attachments'
        properties:
          multiple: true
          allowRemoval: true
          saveToFileMount: '1:/user_upload/'
          allowedMimeTypes:
            - application/pdf
            - image/jpeg

      - type: ImageUpload
        identifier: images
        label: 'Images'
        properties:
          multiple: true
          allowRemoval: true
          saveToFileMount: '1:/user_upload/'
          allowedMimeTypes:
            - image/jpeg
            - image/png
Copied!

The multiple option is also available in the Form Editor backend module as a checkbox in the element's inspector panel.

The allowRemoval property enables users to remove previously uploaded files before submitting the form. When enabled, a Remove checkbox is displayed next to each uploaded file.

File count validation 

The existing Count validator can now be used with FileUpload and ImageUpload elements to limit the number of uploaded files:

fileadmin/form_definitions/someForm.yaml
- type: FileUpload
  identifier: attachments
  label: 'Attachments'
  properties:
    multiple: true
  validators:
    - identifier: Count
      options:
        minimum: 1
        maximum: 5
Copied!

Frontend rendering 

When multiple is enabled:

  • The file input field renders with the HTML5 multiple attribute
  • Previously uploaded files are displayed in a list with individual remove checkboxes
  • Users can select multiple files in the browser's file picker dialog
  • On the summary page, multiple files are displayed as a list

File deletion 

The implementation uses HMAC-signed deletion requests similar to Extbase file handling. Each uploaded file displays a checkbox that, when checked, marks the file for removal on form submission. The deletion data is signed with an HMAC to prevent manipulation.

A new ViewHelper, <formvh:form.uploadDeleteCheckbox> , is available for custom templates:

<formvh:form.uploadDeleteCheckbox
    property="{element.identifier}"
    fileReference="{file}"
    fileIndex="{iterator.index}"
/>
Copied!

Adapting custom finishers for multiple file uploads 

When multiple is enabled on a FileUpload element, the value returned by $formRuntime[$element->getIdentifier()] is an ObjectStorage<FileReference> instead of a single FileReference . Custom finishers that process file uploads need to be adapted to handle both cases, single and multiple uploads.

The following example shows the pattern used in the core EmailFinisher and DeleteUploadsFinisher :

EXT:my_extension/Classes/Domain/Finishers/MyFinisher.php
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;

class MyFinisher extends AbstractFinisher
{
    protected function executeInternal(): void
    {
        $formRuntime = $this->finisherContext->getFormRuntime();

        foreach (
            $formRuntime->getFormDefinition()->getRenderablesRecursively()
            as $element
        ) {
            if (!$element instanceof FileUpload) {
                continue;
            }

            $file = $formRuntime[$element->getIdentifier()];

            // Single file upload: value is a FileReference
            if ($file instanceof FileReference) {
                $this->processFile($file->getOriginalResource());
            }

            // Multiple file upload: value is an ObjectStorage of FileReferences
            if ($file instanceof ObjectStorage) {
                foreach ($file as $singleFile) {
                    if ($singleFile instanceof FileReference) {
                        $this->processFile(
                            $singleFile->getOriginalResource()
                        );
                    }
                }
            }
        }
    }

    private function processFile(FileInterface $file): void
    {
        // Your custom logic, e.g. move, copy, attach, etc.
    }
}
Copied!

Per-element validation with ObjectStorageElementValidatorInterface 

When a form field value is an ObjectStorage , for example, a multiple-file upload, the ProcessingRule must decide how to call each registered validator:

  • Collection-level validators (default) receive the entire ObjectStorage . Use this for validators that check the collection as a whole, such as CountValidator for the minimum or maximum number of items.
  • Element-level validators receive each item individually. Use this for validators that inspect a single item, such as MimeTypeValidator or FileSizeValidator .

To mark a validator as element-level, implement the marker interface ObjectStorageElementValidatorInterface :

EXT:my_extension/Classes/Validation/MyPerFileValidator.php
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Validation\ObjectStorageElementValidatorInterface;

final class MyFileValidator extends AbstractValidator implements
    ObjectStorageElementValidatorInterface
{
    public function isValid(mixed $value): void
    {
        // $value is a single element from the ObjectStorage,
        // e.g. a FileReference - not the whole collection.
    }
}
Copied!

For single-value fields, that is, non- ObjectStorage values, the interface has no effect. Validators are always called with the field value directly.

Impact 

  • Form integrators can now create forms that accept multiple file uploads without custom extensions
  • The FileUpload and ImageUpload elements support the new multiple property
  • All existing finishers, EmailFinisher, SaveToDatabaseFinisher, and DeleteUploadsFinisher, automatically support multiple file uploads
  • Email templates display multiple files as a list of filenames
  • The summary page displays multiple images as a gallery and multiple files as a list of filenames

Feature: #105742 - Synchronized manipulation of all crop variants 

See forge#105742

Description 

The image manipulation wizard allows images to be cropped to multiple crop variants. When many variants were present, each had to be edited individually, requiring the same changes to be applied multiple times. This was particularly tedious when an editor needed identical crop values across all image variants.

This feature introduces a checkbox that allows editors to crop all image variants simultaneously. This checkbox is available if all crop variants share identical aspect ratios and configuration (except for the title).

excludeFromSync is a new sub-option of the cropVariants TCA/TCEFORM configuration array which allows developers to exclude specific crop variants from synchronized cropping.

This is useful, for example, when adding a special crop variant for a list view that has a different configuration, while still allowing other crop variants to be synchronized.

Example 

The following example defines standard crop variants for a Bootstrap-based template.

All crop variants are configured identically (except for the title), which enables synchronized cropping.

An additional crop variant for the tx_news list view is defined. The option excludeFromSync = 1 ensures that this variant is excluded from synchronization.

TCEFORM.sys_file_reference.crop.config.cropVariants {
    xxl {
        title = Very Large Desktop
        selectedRatio = NaN
        allowedAspectRatios {
            # [...] array of defined aspect ratios (identical!)
        }
    }

    xl {
        title = Large Desktop
        selectedRatio = NaN
        allowedAspectRatios {
            # [...] array of defined aspect ratios (identical!)
        }
    }

    # [...]
}

# Override for news extension
TCEFORM.tx_news_domain_model_news.fal_media.config {
    overrideChildTca.columns.crop.config.cropVariants {
        listview {
            title = List view
            selectedRatio = default
            excludeFromSync = 1
            allowedAspectRatios {
                # [...] array of custom aspect ratio definitions
                # (or identical aspect ratios, but not considered for
                # synchronized cropping)
            }
        }
    }
}
Copied!

Impact 

Editors can now apply changes to image aspect ratios and cropping to multiple matching crop variants in the image manipulation wizard.

Specific cropVariants can be excluded from synchronization.

Feature: #105827 - Search in backend page tree and live search can find pages by their frontend URI 

See forge#105827 and forge#105833

Description 

The backend page tree search functionality has been enhanced to allow users to enter a full URI such as https://mysite.example.com/de/any/subtree/page/, which shows the matching page in the result tree.

Multiple URIs can be separated with commas (,), just like multiple page IDs.

It is also possible to combine different search input:

Combining multiple search parts
4,8,https://example.com/first,http://sub.example.com/en/second,anyPageTitle
Copied!

Matches in frontend URIs of translated pages are marked accordingly.

This functionality uses the PSR-14 event BeforePageTreeIsFilteredEvent (see Feature: #105833 - Extended page tree filter functionality) and can serve as inspiration for custom search variations.

In addition, live search has been enhanced to perform the same lookup based on a single URI. This is achieved with the new PSR-14 event ModifyConstraintsForLiveSearchEvent (see Feature: #105827 - New PSR-14 ModifyConstraintsForLiveSearchEvent).

Live search returns both the default language page derived from the URI and the matching translated page.

Configuration 

Search by frontend URI is enabled by default and can be controlled in two ways, similar to search by translation:

User TSconfig 

Administrators can control the availability of frontend URI search with user TSconfig:

# Disable searching by frontend URI for specific users/groups
options.pageTree.searchByFrontendUri = 0
Copied!

User preference 

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

Impact 

Editors can now easily locate a backend page when only the frontend URI is available. Permissions to view or edit the page are respected. Invalid or non-matching URIs are ignored.

Feature: #105827 - New PSR-14 ModifyConstraintsForLiveSearchEvent 

See forge#105827, forge#105833, forge#93494

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyConstraintsForLiveSearchEvent has been added to TYPO3 Core. This event is dispatched in the LiveSearch class and allows extensions to modify the CompositeExpression constraints collected in an array before execution.

This makes it possible to add additional constraints to the main query constraints, combined with a logical OR. These constraints could not previously be accessed by the existing event ModifyQueryForLiveSearchEvent .

The event provides the following methods:

  • getConstraints() : Returns the current array of query constraints (composite expressions).
  • addConstraint() : Adds a single constraint.
  • addConstraints() : Adds multiple new constraints.
  • getTableName() : Returns the table for which the query is executed (for example, pages or tt_content).
  • getSearchDemand() : Returns the search demand.

Example 

The corresponding event listener class:

<?php

namespace Vendor\MyPackage\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\ModifyConstraintsForLiveSearchEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\ConnectionPool;

final readonly class PageRecordProviderEnhancedSearch
{
    public function __construct(
        private ConnectionPool $connectionPool,
    ) {}

    #[AsEventListener('my-package/livesearch-enhanced')]
    public function __invoke(
        ModifyConstraintsForLiveSearchEvent $event,
    ): void {
        if ($event->getTableName() !== 'pages') {
            return;
        }

        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        // Add a constraint so that pages marked with "show_in_all_results=1"
        // will always be shown.
        $constraints[] = $queryBuilder->expr()->eq(
            'show_in_all_results',
            1,
        );

        $event->addConstraints(...$constraints);
    }
}
Copied!

Core itself uses this event to allow searching for frontend URIs in the backend page tree.

Impact 

A new PSR-14 event is now available for adding constraints to the live search query. These constraints are combined with a logical OR.

Feature: #106153 - Improve DebugExceptionHandler with copy functionality 

See forge#106153

Description 

The debugging exception handler, which can be configured for backend and frontend error reporting, provides a large stack trace with details about an error.

This is often essential when reporting bugs in TYPO3 or debugging custom code.

The output has now been improved:

  • Each stack trace segment's file name and the line number where the error occurred now has a "Copy path" button. Clicking it copies the full path, file name, and line number to the browser clipboard.
  • The bottom of the page shows two buttons: one toggles the output above to hide or reveal the file contents, and the other copies the entire stack trace in plain text format so that it can be forwarded in error reports.
  • A brief section explains what a "stack trace" is, and a jump link is available to go from the top of the page to the export section.

Thanks to Olivier Dobberkau, whose extension https://github.com/dkd-dobberkau/enhanced-error-handler inspired the rework of this feature.

Impact 

Errors and their stack traces can now be copied and forwarded much more easily for support requests, without the need to save an HTML file or take screenshots.

File names and line numbers of errors can also be copied easily and inserted into an IDE to jump directly to the relevant code.

Feature: #106261 - Align command line arguments of message consumer with Symfony original 

See forge#106261

Description 

This change aligns the command line arguments of the TYPO3 Console messenger:consume command with the original Symfony Messenger implementation.

The following new options have been added:

  • --limit / -l : Limits the number of received messages.
  • --failure-limit / -f : Limits the number of failed messages the worker can consume.
  • --memory-limit / -m : Sets the memory limit available to the worker.
  • --time-limit / -t : Sets the time limit in seconds during which the worker can handle new messages.
  • --bus / -b : Specifies the name of the bus to which received messages are dispatched.
  • --all : Consumes messages from all receivers.
  • --keepalive : Uses the transport keepalive mechanism, if implemented.

Scheduler integration 

The command can be configured as a scheduler task in TYPO3, enabling automated consumption of messages from the configured transports. This is particularly useful for processing asynchronous messages in the background.

This integration helps projects adopt asynchronous message handling by providing a reliable way to process messages without manual intervention. Messages can be dispatched asynchronously during normal request handling and consumed in the background by the scheduler task, improving application performance and user experience.

Usage 

Consume messages from a specific receiver:

vendor/bin/typo3 messenger:consume my_receiver
Copied!

Consume messages with a message limit:

vendor/bin/typo3 messenger:consume my_receiver --limit=10
Copied!

Stop the worker after 2 failed messages:

vendor/bin/typo3 messenger:consume my_receiver --failure-limit=2
Copied!

Stop the worker when the memory limit is exceeded:

vendor/bin/typo3 messenger:consume my_receiver --memory-limit=128M
Copied!

Stop the worker after a time limit:

vendor/bin/typo3 messenger:consume my_receiver --time-limit=3600
Copied!

Consume from specific queues only:

vendor/bin/typo3 messenger:consume my_receiver --queues=fasttrack
Copied!

Consume from all configured receivers:

vendor/bin/typo3 messenger:consume --all
Copied!

Feature: #106640 - Localize enum labels in site settings definitions 

See forge#106640

Description 

Enum option labels in site settings definitions can now be localized consistently.

This applies to all common enum declaration styles:

  • List-style enum declarations derive localization keys using settings.<settingKey>.enum.<enumValue> in the set labels file.
  • Map-style enum declarations are independent of that key schema. Only the configured label value is evaluated.
  • Map-style enum declarations with localization references (LLL:...) resolve these references.
  • Map-style enum declarations with literal labels keep these labels as-is.
  • Map-style key-only enum entries fall back to the enum value.
  • Map-style empty string labels remain empty strings.

Example 

List-style enum declaration in settings.definitions.yaml
settings:
  my.enumSetting:
    type: string
    default: optionA
    enum:
      - optionA
      - optionB
Copied!
Matching labels in labels.xlf
<trans-unit id="settings.my.enumSetting.enum.optionA">
  <source>Option A (localized)</source>
</trans-unit>
<trans-unit id="settings.my.enumSetting.enum.optionB">
  <source>Option B (localized)</source>
</trans-unit>
Copied!
Map-style enum declaration in settings.definitions.yaml
settings:
  my.enumSetting:
    type: string
    default: optionA
    enum:
      optionA: 'LLL:my_extension.labels:settings.custom.optionA' # Explicit LLL reference
      optionB: 'Literal Option B' # Literal label
      optionC: # Key-only map-style entry, falls back to enum value "optionC"
      optionD: '' # Empty label stays empty
Copied!
Referenced label in labels.xlf
<trans-unit id="settings.custom.optionA">
  <source>Option A (localized)</source>
</trans-unit>
Copied!

If you want to work with automatically derived keys in the set labels.xlf, for example settings.<settingKey>.enum.<enumValue>, omit enum labels in YAML and use list-style enum declarations.

Impact 

Integrators can localize enum options consistently using the same resolution behavior as other setting labels.

Feature: #106681 - Support relative date formats in DateRange validator 

See forge#106681

Description 

The DateRange validator of the form extension now supports relative as well as absolute dates in Y-m-d format.

This allows form integrators to define dynamic date constraints that are evaluated at runtime, such as ensuring a date of birth is at least 18 years in the past or that a date is in the future.

The following relative expressions are supported and follow the syntax of strtotime() :

  • Named dates: today , now , yesterday , tomorrow
  • Relative offsets: -18 years , +1 month , -2 weeks , +30 days

These expressions can be used in the options.minimum and options.maximum properties of the DateRange validator.

Example 

Ensure that a date of birth is at least 18 years in the past:

type: Date
identifier: date-of-birth
label: 'Date of birth'
validators:
  -
    identifier: DateRange
    options:
      maximum: '-18 years'
Copied!

Ensure that a date is in the future:

type: Date
identifier: event-date
label: 'Event date'
validators:
  -
    identifier: DateRange
    options:
      minimum: '+1 day'
Copied!

Mixed absolute and relative dates are also supported:

validators:
  -
    identifier: DateRange
    options:
      minimum: '2020-01-01'
      maximum: 'today'
Copied!

The form editor in the TYPO3 backend has been updated to accept these relative expressions in the date range fields. The HTML min and max attributes on the rendered <input type="date"> element are automatically resolved to absolute Y-m-d dates at render time.

Impact 

Form integrators can now use relative date expressions in the DateRange validator configuration. Existing form definitions using absolute dates will continue to work without changes.

Feature: #106828 - Add user TSconfig to define default live search action 

See forge#106828

Description 

A new user TSconfig option options.liveSearch.actions has been introduced to allow integrators to define the default behavior of a search.

Available actions:

  • edit: Opens the edit form of the record. This is the default for all tables except pages.
  • layout: Opens the page in the Page module. This is the default for the pages table.
  • list: Opens the storage page of the record in the Record List module.
  • preview: Opens the record in the frontend.

Examples 

Set the default for all tables:

options.liveSearch.actions.default = edit
Copied!

Set the default for the tt_content table:

options.liveSearch.actions.tt_content.default = layout
Copied!

Set the default for a custom table:

options.liveSearch.actions.my_table.default = preview
Copied!

Impact 

The default actions of live search results can now be configured with user TSconfig. Integrators can define global and table-specific behavior for search results, improving backend workflows.

The default behavior for pages has been changed to layout to improve the user workflow.

Feature: #107003 - Add event to change record data in list view 

See forge#107003

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\AfterRecordListRowPreparedEvent has been added. This event is dispatched in DatabaseRecordList and allows extensions to modify the data used to render a single record in the list view.

The event allows the following properties to be modified:

  • data: The row fields as an array. The following fields are available:

    • _SELECTOR_: The checkbox element
    • icon: The icon
    • __label: Special field that contains the header
    • _CONTROL_: The row action buttons
    • _LOCALIZATION_: The current language
    • _LOCALIZATION_b: The translated language
    • rowDescription: The row description
    • header: The header. This field is used only if __label is not set. Use __label instead.
    • uid: The record UID (read-only)
  • tagAttributes: The HTML tag attributes of the row. The following attributes are available:

    • class
    • data-table
    • title

The corresponding event listener class:

use TYPO3\CMS\Backend\RecordList\Event\AfterRecordListRowPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('my-package/backend/my-listener-name')]
final class MyEventListener
{
    public function __invoke(AfterRecordListRowPreparedEvent $event): void
    {
        $data = $event->getData();
        $tagAttributes = $event->getTagAttributes();

        // Modify the row data and tag attributes here.

        $event->setData($data);
        $event->setTagAttributes($tagAttributes);
    }
}
Copied!

Impact 

The new PSR-14 event can be used, for example, to modify the title link in the record list.

Feature: #107058 - Simplify registration of a custom form element 

See forge#107058

Description 

The registration of custom form elements in the TYPO3 Form Framework has been simplified. Previously, registering a custom form element required subscribing to the JavaScript event view/stage/abstract/render/template/perform to render the element in the Form Editor stage area.

Custom form elements can now be registered without custom JavaScript code. The Form Editor automatically uses a generic Web Component to render form elements in the stage area.

To use this simplified registration method, omit the formEditorPartials configuration in your form element's YAML definition. The Form Editor then automatically renders the element using the built-in <typo3-form-form-element-stage-item> web component, which provides:

  • Element type and identifier display
  • Element label with required indicator
  • Validator visualization
  • Support for select options (SingleSelect, MultiSelect, RadioButton, Checkbox)
  • Support for allowed MIME types (FileUpload, ImageUpload)
  • Element toolbar
  • Hidden state visualization

The generic rendering automatically extracts and displays relevant information from the form element configuration without requiring a custom template or JavaScript code.

Impact 

Extension developers can now register custom form elements with minimal configuration. By omitting the formEditorPartials configuration, the Form Editor automatically renders the element using a generic Web Component, eliminating the need for:

  • Custom Fluid templates in Resources/Private/Backend/Partials/FormEditor/Stage/
  • Custom JavaScript code subscribing to view/stage/abstract/render/template/perform
  • Manual element rendering logic

This significantly reduces the complexity and maintenance burden when creating custom form elements that do not require special visualization in the Form Editor.

For custom form elements that require specialized rendering or custom interactions in the stage area, the formEditorPartials configuration can still be used to provide custom Fluid templates, which continue to work as before.

For a complete step-by-step tutorial on creating custom form elements, see Creating a Custom Form Element.

Feature: #107289 - Automatic history tracking for Extbase entities 

See forge#107289

Description 

TYPO3 now tracks the history of all Extbase domain entities by listening to Extbase persistence events and storing them in the sys_history table. This provides a comprehensive audit trail for all frontend and backend operations on Extbase entities without requiring any code changes.

The feature leverages TYPO3's existing RecordHistoryStore infrastructure and integrates seamlessly with the backend record history functionality.

The history tracking captures:

  • Create operations: when entities are persisted for the first time
  • Update operations: when existing entities are modified
  • Delete operations: when entities are removed from persistence

All operations are tracked with their proper user context (frontend users, backend users, anonymous operations) and include full entity data snapshots.

Configuration 

History tracking is disabled by default. It can be enabled with the feature toggle extbase.enableHistoryTracking (available via System > Settings > Feature toggles).

Once the feature toggle is enabled, history tracking is active for all Extbase domain model storage tables. It can then be disabled via TCA on a per-table basis:

EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_blog.php
<?php
declare(strict_types=1);

return [
    'ctrl' => [
        'title' => 'my_extension.messages:my_title',
        'label' => 'uid',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        // ...
        'extbase' => [
            'enableHistoryTracking' => false,
        ],
    ],
    'columns' => [
        // ...
    ],
];
Copied!

Defining this at the TCA level (instead of TypoScript persistence configuration) means that it can be configured per table and evaluated consistently in all contexts (backend, frontend, CLI).

If a third-party extension enables history tracking via TCA, it can be disabled using TCA overrides. Disabling the feature toggle also disables all history tracking, even for tables configured with enableHistoryTracking => true.

In addition, the following PSR-14 event listeners can be deregistered or replaced at instance level:

  • extbase-history-tracker-persisted
  • extbase-history-tracker-updated
  • extbase-history-tracker-removed

Impact 

Changes to all Extbase domain entities can now be tracked in the sys_history table, making them visible in the backend record history. This requires enabling the feature toggle extbase.enableHistoryTracking (default: false).

This feature provides administrators and developers with full visibility into data changes without requiring interface implementations or code modifications.

Technical details 

The implementation consists of a PSR-14 event listener ExtbaseHistoryTracker which automatically registers for the following Extbase persistence events:

  • EntityAddedToPersistenceEvent
  • EntityUpdatedInPersistenceEvent
  • EntityRemovedFromPersistenceEvent

All entities with valid TCA configuration are tracked automatically. This uses the Extbase DataMap API, TCA Schema API, and RecordHistoryStore API.

Feature: #107802 - Support username and password in Redis session backend 

See forge#107802

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Before that, authentication was possible by password only. This change means the TYPO3 Redis session backend can be configured as follows:

config/system/additional.php
use TYPO3\CMS\Core\Session\Backend\RedisSessionBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => RedisSessionBackend::class,
    'options' => [
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ],
];
Copied!

Impact 

The password configuration option of the Redis session backend is now typed as array|string. Setting this configuration option to an array is deprecated and will be removed in TYPO3 v15.0.

Feature: #107826 - Introduce Extbase action authorization attribute 

See forge#107826

Description 

A new authorization mechanism has been introduced for Extbase controller actions using PHP attributes. Extension authors can now implement declarative access control logic on action methods using the #[Authorize] attribute.

The #[Authorize] attribute supports multiple authorization strategies:

Built-in checks:

  • Require frontend user login via requireLogin
  • Require specific frontend user groups via requireGroups

Custom authorization logic:

  • Dedicated authorization class (recommended for complex logic)
  • Public controller method (for simple checks)

Multiple #[Authorize] attributes can be stacked on a single action. All authorization checks must pass for access to be granted. If a check fails, a PropagateResponseException is thrown with an HTTP 403 response, which stops the Extbase dispatch process.

Examples 

Require frontend user login 

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    public function listAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Require specific user groups 

The requireGroups parameter accepts an array of frontend user group identifiers. Groups can be specified either by their UID (recommended) or by their title. If multiple groups are specified, the user must be a member of at least one group (OR logic).

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    // Recommended: Use group UIDs
    #[Authorize(requireGroups: [1, 2])]
    public function adminListAction(): ResponseInterface
    {
        // Only accessible to users in groups 1 or 2
        return $this->htmlResponse();
    }

    // Alternative: Use group titles (not recommended)
    #[Authorize(requireGroups: ['administrators', 'editors'])]
    public function editorListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }

    // Mixed: UIDs and titles can be combined (not recommended)
    #[Authorize(requireGroups: [1, 'editors'])]
    public function mixedListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Custom authorization class 

For complex authorization logic, create a dedicated authorization class. This class supports dependency injection and can be reused across controllers. The class must be publicly available in the DI container, which can be achieved by annotating it with #[Autoconfigure(public: true)] .

EXT:my_extension/Classes/Authorization/MyObjectAuthorization.php
namespace MyVendor\MyExtension\Authorization;

use MyVendor\MyExtension\Domain\Model\MyObject;
use TYPO3\CMS\Core\Context\Context;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true)]
class MyObjectAuthorization
{
    public function __construct(
        protected readonly Context $context,
    ) {}

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid()
            === $userAspect->get('id');
    }
}
Copied!
EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function deleteAction(MyObject $myObject): ResponseInterface
    {
        // Delete the object
        return $this->htmlResponse();
    }
}
Copied!

Public controller method 

For simple checks, a public controller method can be used as a callback.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    public function __construct(
        protected readonly Context $context,
    ) {}

    #[Authorize(callback: 'checkOwnership')]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid() === $userAspect->get('id');
    }
}
Copied!

Combining multiple authorization checks 

Multiple #[Authorize] attributes can be stacked. All checks must pass.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    #[Authorize(requireGroups: [1, 2])]
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        // Only accessible to logged-in users in groups 1 or 2 who own the object
        return $this->htmlResponse();
    }
}
Copied!

Authorization checks can be combined within a single attribute:

#[Authorize(requireLogin: true, requireGroups: [1, 2])]
public function adminAction(): ResponseInterface
{
    return $this->htmlResponse();
}
Copied!

Customizing the authorization denied response 

By default, the authorization check throws a PropagateResponseException with an HTTP 403 response. This response can be handled by the TYPO3 page error handler configured in site settings.

The PSR-14 event BeforeActionAuthorizationDeniedEvent can be used to provide a custom PSR-7 response, which is then returned by Extbase.

EXT:my_extension/Classes/EventListener/CustomAuthorizationResponseListener.php
namespace MyVendor\MyExtension\EventListener;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionAuthorizationDeniedEvent;

final class CustomAuthorizationResponseListener
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly StreamFactoryInterface $streamFactory,
    ) {}

    public function __invoke(
        BeforeActionAuthorizationDeniedEvent $event,
    ): void {
        // Customize response based on failure reason
        $message = match ($event->getFailureReason()) {
            AuthorizationFailureReason::NOT_LOGGED_IN =>
                'Please log in to access this page',
            AuthorizationFailureReason::MISSING_GROUP =>
                'You do not have permission to access this page',
            AuthorizationFailureReason::CALLBACK_DENIED =>
                'Access to this resource is denied',
        };

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withStatus(403)
            ->withBody($this->streamFactory->createStream($message));

        $event->setResponse($response);
    }
}
Copied!

Security considerations 

Impact 

Extension authors can now implement secure, declarative authorization checks for Extbase controller actions using the #[Authorize] attribute.

Feature: #107887 - New "Latest backend logins" widget 

See forge#107887

Description 

A new dashboard widget Latest backend logins has been introduced to display recent backend user logins in the TYPO3 Dashboard. This allows administrators to quickly monitor user activity and track recent backend access patterns without navigating the system log.

The widget provides a configurable interface where administrators can set the number of logins to display, offering flexibility in monitoring scope.

Each login entry shows:

  • The backend user avatar and name
  • The login time

Key benefits:

  • Displays recent backend user logins with user details and timestamps
  • Provides direct access to login monitoring without navigating the system log
  • Offers configurable display limits for different monitoring needs
  • Enhances security monitoring with quick access to login patterns

Impact 

This feature improves administrative oversight by providing immediate visibility of recent backend user activity in the dashboard.

Feature: #107906 - Recently opened documents widget 

See forge#107906

Description 

A new Recently Opened Documents dashboard widget has been introduced to display documents that are currently open or recently accessed in the TYPO3 backend. This allows editors and administrators to quickly return to their work and access frequently edited content without navigating through the page tree or search.

The widget provides a configurable interface where users can set the number of documents to display, offering flexibility based on their workflow needs. Each document entry shows the record icon and title for easy identification and quick access.

Key benefits:

  • Displays recently opened documents with icons and titles
  • Provides quick access to ongoing work without searching
  • Offers configurable display limits for different workflow needs
  • Shows document type icons for visual identification
  • Improves editing efficiency by reducing navigation time
  • Displays documents in reverse chronological order (most recent first)

The widget retrieves documents from the FormEngine session data, ensuring that only currently open or recently accessed documents are displayed. Deleted records are filtered out to maintain data accuracy.

Impact 

This feature improves editorial efficiency by providing immediate access to recently opened documents in the dashboard interface, reducing the time spent navigating through the backend to resume work.

Feature: #107940 - Introduce report about content type usage 

See forge#107940

Description 

A new Content statistics module has been introduced in the TYPO3 backend under Reports. This module provides information about the usage of content elements in the TYPO3 site.

Overview 

The overview displays all available content element types along with the number of times each type is used. All the associated fields of the element are listed, including key details such as:

  • The field type
  • Whether it is marked as required
  • Whether it can be configured as excludable via user group permissions

Detail view 

The detail view of each content element type lists all the relevant records that are not marked as deleted.

Impact 

The new report offers a convenient way to analyze and optimize content structures within a TYPO3 installation. It helps administrators and developers to:

  • Identify unused content element types
  • Understand which fields belong to specific content element types
  • Gain insights into the overall configuration and diversity of content elements

Feature: #108345 - Allow extensions without ext_emconf.php in classic mode 

See forge#108345

Description 

Initially ext_emconf.php was the only file providing extension metadata. Since the introduction of composer.json, now mandatory for extensions, there are now two files containing a lot of redundant data.

This is now resolved by allowing an extension's composer.json to contain information that was previously defined in ext_emconf.php:

  1. Extension title and description
  2. Extension version
  3. Extension state / update exclusion
  4. Dependencies on other TYPO3 extensions
  5. PHP version constraints

Extension title and description 

See Feature: #108653 - Database storage for form extension for how the extension title and description can be set individually in composer.json.

Extension version 

The version number can be set in extra.typo3/cms.version or alternatively in the "version" field in composer.json.

For third-party extensions to be compatible with TYPO3 classic mode, this version must now be set to the same version previously defined in ext_emconf.php and should match the version in the Git tag, for example when publishing to Packagist.

Fixture extensions used in tests can set any version number, for example 1.0.0, but a version number must still be provided to avoid deprecation messages. During testing the version number is not evaluated.

TYPO3 Core extensions may omit the version number in composer.json because their version number is derived via \TYPO3\CMS\Core\Information\Typo3Version .

Extension state and update exclusion 

The former state property in ext_emconf.php was used for multiple purposes. In composer.json, this is now represented by dedicated metadata instead of a single field.

Supported extension stability values are expressed as version suffixes, for example:

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.2.3-alpha4",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

Supported Composer stability values are:

  • dev
  • alpha
  • beta
  • RC
  • stable

For example:

  • 1.2.3-dev
  • 1.2.3-alpha1
  • 1.2.3-beta2
  • 1.2.3-RC3
  • 1.2.3

Values from the former state field that are not supported by Composer stability can be expressed as build metadata by appending +... to the version string.

Example:

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.4.2+obsolete",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

In this example, TYPO3 will treat the version as 1.0.0, keep obsolete as build metadata, and expose it in the Extension Manager.

The former state = excludeFromUpdates value from ext_emconf.php is now represented by a dedicated boolean flag in composer.json:

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.2.3",
            "exclude-from-updates": true,
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

This replaces overloading the former state field for update handling.

Dependencies on other TYPO3 extensions 

ext_emconf.php had a property for specifying dependencies on other extensions by referencing the extension key and an optional range of versions.

composer.json also contains a field for specifying dependencies using a Composer package name with a version range. However, there is no direct way to distinguish whether such a package name refers to another TYPO3 extension or to a regular Composer package that should be installed from Packagist.

TYPO3, however, needs to know which other extensions an extension depends on in order to resolve the extension loading order correctly.

Therefore, TYPO3 must know which package names refer to TYPO3 extensions and which refer to regular Composer packages. In Composer mode, this can be resolved automatically.

In classic mode, TYPO3 now recognizes several categories:

  • TYPO3 framework packages shipped by the core
  • Composer packages already installed and shipped with TYPO3
  • Composer packages provided by other loaded extensions via providesPackages

Because of this, extension authors do not need to repeat such package names in providesPackages.

Extensions still need to declare Composer packages that they themselves provide when loaded in classic mode. For those entries, providesPackages can also define a relative path to a Composer vendor directory. If that directory contains a Composer-generated autoload.php, TYPO3 includes it early during bootstrap.

This makes it possible to both declare Composer packages and bootstrap their autoloader in a standardized way.

Here is an example of an extension that ships a local Composer vendor directory:

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "typo3/cms-core": "^14.2",
        "vendor/other-example": "*",
        "symfony/dotenv": "^8.0"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.2.3",
            "Package": {
                "providesPackages": {
                    "symfony/dotenv": "Resources/Private/Php/ComposerVendor"
                }
            }
        }
    }
}
Copied!

In this example, the package symfony/dotenv is provided by the extension itself in TYPO3 classic mode, and TYPO3 will include Resources/Private/Php/ComposerVendor/autoload.php early if it is a Composer-generated autoload file.

The Composer package names typo3/cms-core and vendor/other-example are assumed to refer to TYPO3 extensions, and TYPO3 guarantees that vendor/example is loaded after vendor/other-example. Otherwise, an error is thrown if the extension vendor/other-example does not exist in the system.

Packages that are already shipped by TYPO3 or already provided by another loaded extension do not need to be listed in providesPackages.

Even if an extension does not depend on any Composer packages, it is still required to specify providesPackages in composer.json as an empty object to ensure future compatibility with TYPO3 classic mode and to avoid deprecation messages in TYPO3 v14.

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "typo3/cms-core": "^14.2",
        "vendor/other-example": "*"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.2.3",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

PHP version constraints 

PHP version constraints from ext_emconf.php can also be represented in the require section of composer.json.

Example:

{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "require": {
        "typo3/cms-core": "^14.2",
        "php": "^8.2"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.5.6",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

The PHP dependency is kept as package metadata so TYPO3 classic mode can still evaluate PHP version requirements. However, it is ignored for extension dependency ordering.

Be aware that keeping ext_emconf.php, while no longer directly required by TYPO3, may still be necessary for some tools, such as Tailor or TYPO3 TER. Therefore, for the time being, it is recommended to keep the file and ensure that its information stays in sync with composer.json as outlined above.

However, TYPO3 will not evaluate ext_emconf.php anymore if the required metadata is correctly defined in composer.json and package metadata can be derived from it.

Impact 

Extensions can now omit ext_emconf.php in TYPO3 classic mode. A deprecation message is shown during cache warm-up when ext_emconf.php is present and composer.json is not yet future-proof because it does not contain the required metadata definitions.

Feature: #108557 - TCA option allowedRecordTypes for page types 

See forge#108557

Description 

A new TCA option allowedRecordTypes is introduced for page types to configure which database tables are allowed for specific types (doktype).

EXT:my_extension/Configuration/TCA/Overrides/pages.php
// Allow any record on that page type.
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = ['*'];

// Allow only specific tables on that page type.
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = [
    'tt_content',
    'my_custom_record',
];
Copied!

The array can contain a list of table names or a single asterisk entry (*) to allow all record types.

By default, only the tables pages, sys_category, sys_file_reference, and sys_file_collection are allowed if this option is not overridden.

The defaults are extended if TCA tables enable the option ctrl.security.ignorePageTypeRestriction. Again, this is not considered if allowedRecordTypes is set. These tables must then also be configured there.

Impact 

The allowed record types for pages can now be configured in TCA. This centralizes the configuration for page types and further reduces the need for ext_tables.php, which was used previously.

Feature: #108580 - Improved page module content preview 

See forge#108580

Description 

The page module's content element preview functionality has been enhanced to provide editors with a better visual representation of content elements in the backend.

Sanitized HTML rendering for content 

Content elements with HTML in the bodytext field (such as text, text & images) now display sanitized HTML in the page module preview instead of plain text.

A new \TYPO3\CMS\Core\Html\PreviewSanitizerBuilder has been introduced that creates a sanitizer specifically for backend previews. This sanitizer:

  • Removes clickable links (unwraps <a> tags while preserving their content)
  • Removes heading tags ( <h1> through <h6> ) while preserving their content
  • Allows safe HTML formatting (bold, italic, lists, etc.)

Enhanced bullet list preview 

Content elements of type "bullet list" now render as HTML lists in the preview.

Harmonized menu element rendering 

Preview rendering for menu content elements and "insert records" elements has been harmonized to match the layout used in the record selector wizard. This provides a consistent experience across different parts of the backend.

Impact 

These enhancements improve the editorial experience in the TYPO3 backend by providing clearer, more informative content previews. Editors can now:

  • See formatted HTML content as it will appear to users
  • Quickly identify bullet list structure and content

Feature: #108581 - Record type specific label configuration 

See forge#108581

Description 

Previously, the TCA label configuration (ctrl['label'], ctrl['label_alt'], and ctrl['label_alt_force']) applied globally to all record types in a table. This meant that all content elements in tt_content , regardless of their CType, displayed the same field(s) as their label in the backend.

It is now possible to define type-specific label configuration in the TCA types section. These settings override global ctrl label configuration for a record type:

  • label - Primary field used for the record title
  • label_alt - Alternative field(s) used when label is empty (or as additional fields)
  • label_alt_force - Force display of alternative fields alongside the primary label

This is especially useful for tables like tt_content where different content element types may benefit from showing different fields. For example, an "Image" content element could display the image caption, while a "Text" element would show the header field.

Examples 

EXT:my_extension/Configuration/TCA/tx_my_table.php
return [
    'ctrl' => [
        'label' => 'header',
        'type' => 'record_type',
        // ... other ctrl configuration
    ],
    'types' => [
        'article' => [
            'label_alt' => 'teaser',
        ],
        'event' => [
            'label_alt' => 'event_date,location',
            'label_alt_force' => true,
        ],
    ],
    // ... columns configuration
];
Copied!

In this example:

  • All types use header as the primary label field (from ctrl['label']).
  • The article type displays the teaser field if header is empty.
  • The event type displays header together with event_date and location (since label_alt_force is enabled).

When adding a new record type to a table, label configuration can be provided as the third argument $additionalTypeInformation of \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType .

EXT:my_extension/Configuration/TCA/Overrides/tx_my_table.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'LLL:frontend.ttc:CType.shortcut',
        'value' => 'my-type',
        'icon' => 'my-icon',
        'group' => 'special',
    ],
    'my-header',
    [
        'label' => 'header',
        'label_alt' => 'records',
    ]
);
Copied!

Impact 

Tables with multiple record types can now define more specific and descriptive labels 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 displayed.

This feature is especially useful for:

  • Content element tables such as tt_content with different CType values
  • Tables with different record types serving different purposes
  • Plugin records with varying functionality for each type
  • Any table where the record type changes the record's purpose or meaning

All occurrences of \TYPO3\CMS\Backend\Utility\BackendUtility::getRecordTitle() in TYPO3 automatically benefit from this feature without any code changes. This includes record lists, page trees, history views, workspaces, and similar backend modules.

Functionality such as FormEngine records cannot benefit from this option yet, as FormEngine does not support the Schema API yet.

Feature: #108648 - Option to modify src attribute for Vimeo/YouTube 

See forge#108648

Description 

The new configuration option srcAttribute for the YouTubeRenderer and VimeoRenderer can be used to modify the previously hard-coded src attribute in the resulting iframe HTML code. This can be useful if the iframe should not be immediately loaded because of privacy concerns. An alternative such as data-src can be used in the initial HTML markup.

Example:

<f:media
    file="{youtubeVideo}"
    additionalConfig="{srcAttribute: 'data-src'}"
/>
Copied!

Impact 

The src attribute for YouTube and Vimeo embeds can now be renamed.

Feature: #108653 - Database storage for form extension 

See forge#108653

Description 

The typo3/cms-form extension has been extended to include a new database storage adapter ( DatabaseStorageAdapter ), allowing form definitions to be stored in the database table form_definition instead of relying on file system storage only.

Form definitions can now be stored in three ways:

  • Database storage (new, recommended) – stored as records in the form_definition table
  • File mounts (FAL) – stored as .form.yaml files in FAL storage (deprecated, see Deprecation: #108653 - Form file-based storage deprecated)
  • Extension paths – shipped with extensions (read-only or configurable)

Storage adapter architecture 

The storage layer uses the Chain of Responsibility pattern. Each storage adapter implements the StorageAdapterInterface and declares which persistence identifiers it can handle via its supports() method. The StorageAdapterFactory iterates through all registered adapters sorted by priority and delegates to the first matching adapter.

Three adapters are shipped:

  • DatabaseStorageAdapter (priority 100)
  • ExtensionStorageAdapter (priority 75)
  • FileMountStorageAdapter (priority 50, deprecated)

Database table form_definition  

A new TCA-managed table form_definition stores the form definitions with the following fields:

  • label – the human-readable form name
  • identifier – the unique form identifier (e.g., contact-form)
  • configuration – the full form definition as JSON

Records are read-only in the standard TCA editing interface. All write and delete operations go through DataHandler , ensuring proper permission checks, history tracking, and hook execution.

Form Manager wizard 

A new Storage wizard step lets editors choose the storage type (file mount, extension, database) when creating or duplicating forms. When only one storage adapter is accessible, the step auto-advances.

The Form Manager now also shows a record history action in the dropdown menu for database-stored forms, linking to the TYPO3 record history module.

Record list integration 

Two event listeners customize the record list of form_definition records:

  • The standard edit action is replaced with a link to the Form Editor module.
  • The standard delete action is removed. Deletion is only possible through the Form Manager.
  • Clicking the record title opens the Form Editor instead of the TCA editing form.

Creation of form_definition records via the "New Record" wizard is denied via page TSconfig:

mod.web_list.deniedNewTables := addToList(form_definition)
Copied!

CLI command: transfer between storages 

A new CLI command form:definition:transfer allows form definitions to be transferred between any two storage backends. This is particularly useful for migrating file-based forms to database storage via the command line.

# Transfer all forms from file mounts to database
bin/typo3 form:definition:transfer --source=filemount --target=database

# Transfer a specific form by its identifier
bin/typo3 form:definition:transfer --source=extension --target=database --form-identifier=contact

# Move forms (transfer + delete from source)
bin/typo3 form:definition:transfer --source=filemount --target=database --move

# Dry-run: preview what would be transferred without making changes
bin/typo3 form:definition:transfer --source=filemount --target=database --dry-run

# Transfer to a specific target location (PID for database storage)
bin/typo3 form:definition:transfer --source=filemount --target=database --target-location=0
Copied!

Available options:

  • --source – source storage type (database, extension, filemount)
  • --target – target storage type
  • --target-location – target storage location
  • --form-identifier – transfer only a specific form
  • --move – delete the source form after successful transfer
  • --dry-run – preview without making changes

Configuration 

Backend users must have table access rights for the form_definition table.

Impact 

Editors can store new form definitions in the database by selecting the "Database" storage type in the Form Manager creation wizard.

File-based storage (file mounts) will remain functional during the deprecation period but will trigger E_USER_DEPRECATED errors. See Deprecation: #108653 - Form file-based storage deprecated for migration instructions. Existing file-based forms are not affected by this change.

Extension-based storage will continue to work without change.

Feature: #108720 - QR code button for frontend preview 

See forge#108720

Description 

A new QR code button has been added next to the View button in various backend modules. Clicking the button opens a modal displaying a scannable QR code for the frontend preview URI.

The button is available in the following locations:

  • Content > Web module (Layout view and Language Comparison view)
  • Web > List module
  • Web > View module
  • Web > Workspaces module

Inside a workspace a QR code contains a special preview URI that will work without backend authentication. This makes it easy to share workspace previews with colleagues and clients, or to quickly check draft versions on a mobile device by scanning the code.

The QR code can be downloaded as PNG or SVG from the modal.

Impact 

Editors can benefit from a streamlined workflow by sharing page previews and testing pages on mobile devices. Workspace-aware preview URIs eliminate the need to be logged in when scanning the QR code, making it particularly useful for reviewing processes involving external stakeholders.

Feature: #108726 - Add PSR-14 events ModifyRenderedContentAreaEvent and ModifyRenderedRecordEvent 

See forge#108726

Description 

The \TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent allows developers to intercept the rendering of individual records in Fluid templates and modify the output. This depends on records being rendered with the new <f:render.contentArea> , see Introduce Fluid f:render.contentArea ViewHelper, or <f:render.record> ViewHelpers in Fluid templates, see Introduce Fluid f:render.record ViewHelper.

Note that any alterations will be output as is and will not be escaped. If you process insecure content inside an event listener, be sure to escape it properly, for example by applying htmlspecialchars() to it.

Example 

An example event listener could look like this:

EXT:my_extension/Classes/EventListener/ModifyRenderedContentEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent;
use TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent;

final class ModifyRenderedContentEventListener
{
    #[AsEventListener]
    public function modifyContentArea(ModifyRenderedContentAreaEvent $event): void
    {
        $content = 'before area<hr />' . $event->getRenderedContentArea()
            . '<hr />after area';
        $event->setRenderedContentArea($content);
    }

    #[AsEventListener]
    public function modifyRecord(ModifyRenderedRecordEvent $event): void
    {
        $content = 'before record<hr />' . $event->getRenderedRecord()
            . '<hr />after record';
        $event->setRenderedRecord($content);
    }
}
Copied!

Impact 

The new events can be used by extension authors to enhance the output of content areas and records rendered in themes.

Feature: #108726 - Introduce Fluid f:render.contentArea ViewHelper 

See forge#108726

Description 

Instead of using <f:cObject> and <f:for> ViewHelpers to render content areas, the new <f:render.contentArea> ViewHelper can be used.

It allows content areas to be rendered while enabling other extensions to modify the output via PSR-14 EventListeners.

This is especially useful for adding debugging wrappers or additional HTML structure around content areas.

By default, the ViewHelper renders the content area as-is, but EventListeners can listen to the ModifyRenderedContentAreaEvent and modify the output.

You need to use the PAGEVIEW config like this:

page = PAGE
page.10 = PAGEVIEW
page.10.paths.10 = EXT:my_site_package/Resources/Private/Templates/
Copied!
MyPage.fluid.html
<f:render.contentArea contentArea="{content.left}"/>
or
{content.left -> f:render.contentArea()}
Copied!

The ViewHelper also supports wrapping each content element with additional markup if combined with the <f:render.record> ViewHelper:

MyPage.fluid.html
<f:render.contentArea contentArea="{content.main}" recordAs="record">
    before {record.fullType}
    <f:render.record record="{record}" />
    after {record.fullType}
</f:render.contentArea>
Copied!

Impact 

Theme creators are encouraged to use the <f:render.contentArea> ViewHelper to allow other extensions to modify the output via EventListeners.

Feature: #108726 - Introduce Fluid f:render.record ViewHelper 

See forge#108726

Description 

Instead of using the <f:cObject> ViewHelper to render database records, the new <f:render.record> ViewHelper can be used.

It allows records to be rendered while enabling other extensions to modify the output via PSR-14 event listeners.

This is especially useful for adding debugging wrappers or additional HTML structure around content elements.

By default, the ViewHelper renders the record as is, but event listeners can listen to the ModifyRenderedRecordEvent and modify the output.

Usage with the record-transformation data processor:

dataProcessing {
    10 = record-transformation
}
Copied!
MyContentElement.fluid.html
<f:render.record record="{record}" />
or
{record -> f:render.record()}
Copied!

You can render not only tt_content records, but any database record by defining the rendering in TypoScript.

# Example TypoScript configuration for rendering custom records
sys_category = FLUIDTEMPLATE
sys_category {
  file = EXT:my_extension/Resources/Private/Templates/Category.html
  layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts/
  partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/
  dataProcessing.1421884800 = record-transformation
}

# Example TypoScript configuration for special record types
tx_myextension_domain_model_product = COA
tx_myextension_domain_model_product.default = FLUIDTEMPLATE
tx_myextension_domain_model_product.default {
  templateName >
  templateName.ifEmpty.cObject = TEXT
  templateName.ifEmpty.cObject {
    field = record_type
    required = 1
    case = uppercamelcase
  }
  # for record_type = 'mainProduct' the template file my_extension/Resources/Private/Templates/Product/MainProduct.html will be used
  layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts/
  partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/
  templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/Product/
  dataProcessing.1421884800 = record-transformation
}
Copied!

Impact 

Theme creators are encouraged to use the <f:render.record> ViewHelper to allow other extensions to modify the output via event listeners.

Feature: #108763 - Console command to analyze Fluid templates 

See forge#108763

Description 

The typo3 fluid:analyze console command is introduced, which analyzes Fluid templates in the current project for correct Fluid syntax and reports deprecations that are emitted during template parsing.

Usage:

vendor/bin/typo3 fluid:analyze
Copied!

Example output:

[DEPRECATION] packages/myext/Resources/Private/Templates/Test.fluid.html: <my:obsolete> has been deprecated in X and will be removed in Y.
[ERROR] packages/myext/Resources/Private/Templates/Test2.fluid.html: Variable identifiers cannot start with a "_": _temp
Copied!

In its initial implementation, the command automatically finds all Fluid templates within the current project based on the *.fluid.* file extension (see Feature: #108166 - Fluid file extension and template resolving) and analyzes them. By default, TYPO3 system extensions are skipped. This can be adjusted by specifying the --include-system-extensions CLI option.

The following errors and deprecations are currently supported:

If exceptions are caught during the parsing process of at least one template, the console command will have a return status of 1 (error). Otherwise, it will return 0 (success). This means that deprecations are not interpreted as errors.

This should make it possible to use the command in CI workflows of most projects, since deprecated functionality used by third-party templates will not make the pipeline fail.

Verbose output allows users to get feedback on the analyzed templates and the number of errors and deprecations, or success.

Integration with other tools 

The command also supports input of a template string via STDIN as well as machine-readable output as JSON. This enables better integration with other development-related tools.

Usage:

echo "<formvh:form.timePicker /> {_invalidVariable}" | vendor/bin/typo3 fluid:analyze --stdin --json
Copied!

Example output (formatted):

{
    "identifier": "template__5adb1a7702b9dcbf",
    "path": "php:\/\/stdin",
    "errors": [
        {
            "file": "\/var\/www\/html\/vendor\/typo3fluid\/fluid\/src\/Core\/Parser\/TemplateParser.php",
            "line": 130,
            "message": "Fluid parse error in template php:\/\/stdin, line 2 at character 27. Error: Variable identifiers cannot start with a \"_\": _invalidVariable (error code 1765900762). Template source chunk:    {_invalidVariable}\n",
            "templateLocation": {
                "identifierOrPath": "php:\/\/stdin",
                "line": 2,
                "character": 27
            }
        }
    ],
    "deprecations": [
        {
            "file": "\/var\/www\/html\/typo3\/sysext\/form\/Classes\/ViewHelpers\/Form\/TimePickerViewHelper.php",
            "line": 143,
            "message": "The TimePickerViewHelper is deprecated since TYPO3 v14 and will be removed in v15."
        }
    ]
}
Copied!

Deprecating ViewHelpers 

The fluid:analyze console command can catch deprecations of whole ViewHelpers if the deprecation is emitted during the parse time of a template. This is possible by implementing the ViewHelperNodeInitializedEventInterface :

ObsoleteViewHelper.php
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperNodeInitializedEventInterface;

/**
 * @deprecated since X, will be removed in Y.
 */
final class ObsoleteViewHelper extends AbstractViewHelper implements ViewHelperNodeInitializedEventInterface
{
    // ...

    public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
    {
        trigger_error(
            '<my:obsolete> has been deprecated in X and will be removed in Y.',
            E_USER_DEPRECATED,
        );
    }
}
Copied!

Deprecating ViewHelper arguments 

The ViewHelperNodeInitializedEventInterface can be used to deprecate a ViewHelper argument. The deprecation is only triggered if the argument is actually used in a template.

SomeViewHelper.php
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperNodeInitializedEventInterface;

final class SomeViewHelper extends AbstractViewHelper implements ViewHelperNodeInitializedEventInterface
{
    public function initializeArguments(): void
    {
        // @deprecated since X, will be removed in Y.
        $this->registerArgument('obsoleteArgument', 'string', 'Original description. Deprecated since X, will be removed in Y');
    }

    public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
    {
        if (array_key_exists('obsoleteArgument', $arguments)) {
            trigger_error(
                'ViewHelper argument "obsoleteArgument" in <my:some> is deprecated since X and will be removed in Y.',
                E_USER_DEPRECATED,
            );
        }
    }
}
Copied!

Impact 

The new typo3 fluid:analyze console command can be used to check basic validity of Fluid templates in projects that use the *.fluid.* file extension and to discover deprecated functionality in template files.

Feature: #108776 - Allow to set user interface language when using CLI to create user 

See forge#108776

Description 

The CLI command typo3 backend:user:create now supports the --language option, or -l , that sets the desired user interface language.

vendor/bin/typo3 backend:user:create --language=de
Copied!

User creation using environment variables:

TYPO3_BE_USER_NAME=username \
TYPO3_BE_USER_EMAIL=admin@example.com \
TYPO3_BE_USER_GROUPS=<comma-separated-list-of-group-ids> \
TYPO3_BE_USER_LANGUAGE=de \
TYPO3_BE_USER_ADMIN=0 \
TYPO3_BE_USER_MAINTAINER=0 \
vendor/bin/typo3 backend:user:create --no-interaction
Copied!

Feature: #108796 - Centralize bookmark management 

See forge#108796

Description 

The TYPO3 backend bookmark system has been comprehensively overhauled, introducing a centralized architecture that replaces the legacy "shortcut" implementation.

Bookmark groups 

Bookmarks can be organized into three types of groups.

System groups are defined via user TSconfig using options.bookmarkGroups and are available to all users. Global groups contain bookmarks visible to all backend users, though only administrators can add bookmarks to these groups. User groups are custom groups created by individual users for their own personal organization and are stored in a new database table sys_be_shortcuts_group .

Five default bookmark groups are provided out of the box: Pages, Records, Files, Tools, and Miscellaneous. Previously these groups were hardcoded in PHP, but they are now defined via user TSconfig in EXT:backend, making them fully customizable. The functionality remains the same, but administrators now have complete control over which groups are available.

The options.bookmarkGroups setting should only be modified in the global scope and not on a per-user basis, as inconsistent group configuration between users can lead to unexpected behavior:

EXT:my_ext/Configuration/user.tsconfig
# Remove a specific default group (for example, Files)
options.bookmarkGroups.3 >

# Remove all default groups
options.bookmarkGroups >

# Add a custom group with a static label
options.bookmarkGroups.10 = My Custom Group

# Add a custom group with a translatable label using domain syntax
options.bookmarkGroups.11 = my_extension.messages:bookmark_group.custom

# Disable bookmarks entirely
options.enableBookmarks = 0
Copied!

Group labels support the TYPO3 translation domain syntax, allowing extensions to provide translated group names. The format is extension_key.messages:translation_key , which resolves to the default language file at EXT:extension_key/Resources/Private/Language/locallang.xlf.

As before, group ID -100 has special behavior as it is a superglobal group. Bookmarks assigned to this group are visible to all backend users, but only administrators can add or modify bookmarks in this group. This allows administrators to provide a shared set of bookmarks across an entire TYPO3 installation.

Bookmark Manager 

A new modal-based Bookmark Manager provides a centralized interface for managing bookmarks. The manager supports drag-and-drop reordering to reorganize bookmarks within and across groups. Bulk operations allow selecting multiple bookmarks to move or delete at once. Users can create, edit, and delete custom bookmark groups through the group management interface, and rename bookmarks via inline editing.

The Bookmark Manager can be accessed via the bookmark icon in the toolbar dropdown menu.

Impact 

The bookmark toolbar item now opens a dropdown with quick access to recent bookmarks and a link to the full Bookmark Manager. Users can create custom bookmark groups to better organize their saved pages, records, and modules. Administrators can configure global bookmarks visible to all users. The Bookmarks dashboard widget has been updated to support the new bookmark system with group filtering and limit options. The legacy "shortcut" terminology has been replaced with "bookmark" throughout the backend interface and code base.

Feature: #108799 - LocalizationRepository methods for fetching record translations 

See forge#108799

Description 

TYPO3 has historically provided helper methods for localization in various places in the TYPO3 code. This patch centralizes localization-related functionality by marking \TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository as public (non-internal) and adding new methods as modern, DI-friendly alternatives to the static BackendUtility methods.

getRecordTranslation() 

Fetches a single translated version of a record for a specific language.

public function getRecordTranslation(
    string|TcaSchema $tableOrSchema,
    int|array|RecordInterface $recordOrUid,
    int|LanguageAspect $language,
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): ?RawRecord
Copied!

getRecordTranslations() 

Fetches all translations of a record. This method can also be used to count translations by using count() on the result, replacing the need for BackendUtility::translationCount() .

public function getRecordTranslations(
    string|TcaSchema $tableOrSchema,
    int|array|RecordInterface $recordOrUid,
    array $limitToLanguageIds = [],
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): array
Copied!

Returns an array of translated RawRecord objects indexed by language ID.

getPageTranslations() 

Fetches all page translations for a page.

public function getPageTranslations(
    int $pageUid,
    array $limitToLanguageIds = [],
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): array
Copied!

Returns an array of page translation records as RawRecord objects indexed by language ID.

Impact 

Extension developers working with record translations in the TYPO3 backend now have access to modern, injectable repository methods that follow current TYPO3 coding practices.

The legacy static methods BackendUtility::getRecordLocalization() , BackendUtility::getExistingPageTranslations() , and BackendUtility::translationCount() remain available for backward compatibility until they are migrated completely.

Feature: #108815 - CLI commands for system configuration 

See forge#108815

Description 

New CLI commands have been introduced to manage TYPO3 system configuration (stored in config/system/settings.php) directly from the command line.

The following commands are now available:

configuration:show 

Shows a configuration value. By default, if the active value differs from the local value (for example, due to overrides in config/system/additional.php), both values are displayed with the difference highlighted.

# Show configuration (with diff if overridden)
vendor/bin/typo3 configuration:show SYS/sitename

# Show active (effective runtime) value
vendor/bin/typo3 configuration:show SYS/sitename --type=active

# Show local (settings.php) value only
vendor/bin/typo3 configuration:show DB/Connections/Default --type=local

# Output as JSON
vendor/bin/typo3 configuration:show BE/debug --type=active --json
Copied!

configuration:set 

Sets a configuration value in config/system/settings.php.

# Set a string value
vendor/bin/typo3 configuration:set SYS/sitename "My Site"

# Set boolean or integer values using --json
vendor/bin/typo3 configuration:set BE/debug true --json
vendor/bin/typo3 configuration:set SYS/displayErrors 1 --json

# Set an array value
vendor/bin/typo3 configuration:set EXTENSIONS/my_extension '{"key": "value"}' --json
Copied!

The --json option parses the value as JSON, which allows booleans, integers, and arrays to be set with proper types.

configuration:remove 

Removes configuration value or values from config/system/settings.php.

# Remove a single path (asks for confirmation)
vendor/bin/typo3 configuration:remove EXTENSIONS/my_extension/setting

# Remove without confirmation
vendor/bin/typo3 configuration:remove EXTENSIONS/my_extension/setting --force

# Remove multiple paths (comma-separated)
vendor/bin/typo3 configuration:remove "EXTCONF/ext1,EXTCONF/ext2" --force
Copied!

Impact 

These commands provide a convenient way to manage TYPO3 system configuration from the command line, which is especially useful for:

  • automated deployment and provisioning scripts
  • CI/CD pipelines that need to adjust configuration
  • quick configuration changes without needing to access the Install Tool
  • scripting and automation tasks

The commands respect TYPO3 configuration path restrictions and only allow writing to paths that are defined in the default configuration or explicitly allowed (such as EXTENSIONS , EXTCONF , DB ).

Feature: #108817 - Introduce web component-based form editor tree 

See forge#108817

Description 

The Form Editor tree component has been completely modernized. It has been migrated from a legacy jQuery-based implementation to a modern web component architecture using Lit and the TYPO3 backend tree infrastructure.

Enhanced user experience 

The new tree component provides a significantly improved user experience with modern interaction patterns and visual feedback:

Intuitive drag and drop
Form elements can now be reorganized using a smooth drag-and-drop interface with intelligent validation rules. The tree automatically prevents invalid operations, such as dragging the root form element, moving pages outside their designated level, or dropping elements into non-composite types.
Smart element organization
Only composite elements like grid containers and fieldsets can receive child elements, while simple form fields remain non-droppable. Pages must always stay at the top level, ensuring proper form structure. The tree automatically distinguishes between reordering siblings and changing parent elements, providing precise control over form organization.
Visual feedback
Clear visual indicators show valid drop zones during drag operations. Selected elements are highlighted with proper styling. The tree provides immediate feedback for all interactions, making form building more intuitive.
Persistent navigation
The tree remembers expanded and collapsed states. After drag and drop operations, the tree maintains your current view and selection, preventing disorientating resets. Navigation feels natural and responsive.
Integrated search
A built-in search toolbar allows quick filtering of form elements by name. The search works client-side for instant results, making it easy to locate specific elements in complex forms.
Collapse-all functionality
The toolbar includes a convenient button to collapse all expanded nodes at once, helping to get a quick overview of your form structure or reset the view to a clean state.

Technical implementation 

The new implementation leverages the proven TYPO3 backend tree infrastructure.

Impact 

Form editors will immediately notice the improved responsiveness and modern feel of the tree component. Drag-and-drop operations are smoother and more predictable. The search functionality makes working with large forms significantly easier. The tree maintains its state during operations, reducing friction and improving workflow efficiency.

The new web component-based architecture ensures better maintainability and extensibility for future enhancements. The component integrates seamlessly with the existing Form Editor without requiring changes to form definitions or configuration.

Feature: #108819 - RecordFieldPreviewProcessor for custom PreviewRenderers 

See forge#108819

Description 

A new service \TYPO3\CMS\Backend\Preview\RecordFieldPreviewProcessor has been introduced to provide common field rendering helpers for custom content element preview renderers.

Previously, these helper methods were only available in StandardContentPreviewRenderer , which required custom preview renderers to extend that class to access them.

Instead, this service uses the composition-over-inheritance pattern.

The new service provides the following methods:

prepareFieldWithLabel() 

Renders a field value with its TCA label prepended in bold.

use TYPO3\CMS\Core\Domain\RecordInterface;

public function prepareFieldWithLabel(
    RecordInterface $record,
    string $fieldName,
): ?string
Copied!

prepareField() 

Renders a processed field value without a label.

use TYPO3\CMS\Core\Domain\RecordInterface;

public function prepareField(
    RecordInterface $record,
    string $fieldName,
): ?string
Copied!

prepareText() 

Processes larger text fields (for example, RTE content) with truncation and HTML stripping.

use TYPO3\CMS\Core\Domain\RecordInterface;

public function prepareText(
    RecordInterface $record,
    string $fieldName,
    int $maxLength = 1500,
): ?string
Copied!

preparePlainHtml() 

Renders plain HTML content with line limiting.

use TYPO3\CMS\Core\Domain\RecordInterface;

public function preparePlainHtml(
    RecordInterface $record,
    string $fieldName,
    int $maxLines = 100,
): ?string
Copied!

prepareFiles() 

Renders thumbnails for file references.

use TYPO3\CMS\Core\Resource\FileReference;

public function prepareFiles(
    iterable|FileReference $fileReferences,
): ?string
Copied!

linkToEditForm() 

Wraps content in an edit link if the user has the appropriate permissions.

use TYPO3\CMS\Core\Domain\RecordInterface;
use Psr\Http\Message\ServerRequestInterface;

public function linkToEditForm(
    string $linkText,
    RecordInterface $record,
    ServerRequestInterface $request,
): string
Copied!

Impact 

Extension developers implementing custom preview renderers can now inject RecordFieldPreviewProcessor to access common field rendering helpers without extending StandardContentPreviewRenderer .

Example usage:

use TYPO3\CMS\Backend\Preview\PreviewRendererInterface;
use TYPO3\CMS\Backend\Preview\RecordFieldPreviewProcessor;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;

final class MyCustomPreviewRenderer implements PreviewRendererInterface
{
    public function __construct(
        private readonly RecordFieldPreviewProcessor $fieldProcessor,
    ) {}

    public function renderPageModulePreviewContent(
        GridColumnItem $item,
    ): string {
        $record = $item->getRecord();
        $content = $this->fieldProcessor->prepareFieldWithLabel(
            $record,
            'header',
        );
        $content .= $this->fieldProcessor->prepareFiles($record->get('image'));

        return $content;
    }
}
Copied!

Feature: #108826 - Add Short URL module 

See forge#108826

Description 

A new backend module Link Management > Short URLs has been introduced. It enables editors to create and manage short URLs that redirect visitors to a configurable target. Short URLs are stored as sys_redirect records with a dedicated record type short_url , providing a streamlined editing form that hides redirect-specific fields that are irrelevant to short URL use cases.

Creating short URLs 

Short URLs can be created in two ways:

  • Manual entry: Editors type a custom path (for example, /promo) into the source path field.
  • Auto-generation: Clicking the Generate Short URL button generates a random 8-character path (for example, /aBcDeFgH). The path is guaranteed to be unique due to server-side collision checking.

Uniqueness enforcement 

Short URL paths must be unique to each source host. Duplicate detection happens at two levels:

  • Client-side validation: While editing, the source path and source host fields are validated against existing records. If a conflict is detected, both fields are highlighted with an error state together with a notification.
  • Server-side enforcement: On save, the DataHandler rejects duplicate short URLs and displays a flash message, ensuring data integrity even if client-side validation is bypassed.

Immutability 

Once a short URL record has been saved, the source path and source host fields become read-only. This ensures that published short URLs remain stable and previously shared links continue to work. The redirect target can be changed at any time.

Clipboard support 

The full short URL, including protocol and host, can be copied to the clipboard from both the list overview and the record editing form.

Impact 

Editors benefit from a dedicated interface for managing short URLs without needing to understand redirect configuration details. The module provides a central location for creating, reviewing, and maintaining short URLs with built-in safeguards against duplicates and accidental modifications.

Feature: #108832 - Introduce UserSettings object for backend user profile settings 

See forge#108832

Description 

A new \TYPO3\CMS\Core\Authentication\UserSettings object provides structured access to backend user profile settings defined in $GLOBALS['TYPO3_USER_SETTINGS'] .

UserSettings object 

The UserSettings object can be retrieved via the backend user:

/** @var \TYPO3\CMS\Core\Authentication\UserSettings $userSettings */
$userSettings = $GLOBALS['BE_USER']->getUserSettings();

// Check if a setting exists
if ($userSettings->has('colorScheme')) {
    $scheme = $userSettings->get('colorScheme');
}

// Get all settings as an array
$allSettings = $userSettings->toArray();

// Typed access via dedicated methods
$emailOnLogin = $userSettings->isEmailMeAtLoginEnabled();
$showUploadFields = $userSettings->isUploadFieldsInTopOfEBEnabled();
Copied!

The class implements ContainerInterface with has() and get() methods. The get() method throws UserSettingsNotFoundException if the setting does not exist.

New JSON storage with backward compatibility 

Profile settings are now stored in a new be_users.user_settings JSON field, providing a structured and queryable format. For backward compatibility, the existing serialized uc blob continues to be written alongside:

// Writing still uses the uc mechanism
$GLOBALS['BE_USER']->uc['colorScheme'] = 'dark';
$GLOBALS['BE_USER']->writeUC();
// Both uc (serialized) and user_settings (JSON) are updated
Copied!

An upgrade wizard, "Migrate user profile settings to JSON format", migrates existing settings from the uc blob to the new user_settings field.

Impact 

Backend user profile settings can now be accessed via the UserSettings object, providing type safety and IDE support. The new JSON storage format improves data accessibility while maintaining full backward compatibility through dual-write to both storage formats.

Feature: #108842 - Add badge for slide mode in Layout module 

See forge#108842

Description 

This feature introduces a visual badge in the Content > Layout module to indicate when slide mode is active. The badge is a clear indicator for editors, enhancing the user experience by providing immediate feedback on the current mode of operation.

Each slide mode has a corresponding badge and description text:

  • For slideMode = none , no badge is shown.
  • For slideMode = slide , a badge with the text "Slide" is shown, but only if there are no content elements on the current page.
  • For slideMode = collect , a badge with the text "Collect" is shown.
  • For slideMode = collectReverse , a badge with the text "CollectReverse" is shown.

Feature: #108843 - User settings configuration migrated to TCA 

See forge#108843 See forge#108832

Description 

The backend user profile settings configuration that was previously stored in $GLOBALS['TYPO3_USER_SETTINGS'] is now available in TCA at $GLOBALS['TCA']['be_users']['columns']['user_settings'] .

This allows user settings to benefit from TCA-based tooling and provides a consistent API that extensions already use for other configuration.

A new method ExtensionManagementUtility::addUserSetting() has been introduced to simplify adding custom fields to user profile settings.

Impact 

Extensions can add custom fields to backend user profile settings using the new addUserSetting() method in Configuration/TCA/Overrides/be_users.php:

// Configuration/TCA/Overrides/be_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserSetting(
    'myCustomSetting',
    [
        'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
        ],
    ],
    'after:emailMeAtLogin'
);
Copied!

Alternatively, extensions can directly modify the TCA:

// Configuration/TCA/Overrides/be_users.php
$GLOBALS['TCA']['be_users']['columns']['user_settings']['columns']['myCustomSetting'] = [
    'label' => 'LLL:my_extension.messages:myCustomSetting',
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
    ],
];

// Add to showitem
$GLOBALS['TCA']['be_users']['columns']['user_settings']['showitem'] .= ',myCustomSetting';
Copied!

Structure 

The user_settings TCA column has the following structure:

columns

Array of field configurations, each containing:

label
The field label (LLL reference or string)
config
Standard TCA config array (type, renderType, items, etc.)
table (optional)
Set to 'be_users' if the field is stored in a be_users table column
showitem
Comma-separated list of fields to display; supports --div--; for tabs

Available field types 

  • input - Text input field
  • number - Number input field
  • email - Email input field
  • password - Password input field
  • check with renderType => 'checkboxToggle' - Checkbox or toggle
  • select with renderType => 'selectSingle' - Select field
  • language - Language selector

Backward compatibility 

For backward compatibility, the legacy $GLOBALS['TYPO3_USER_SETTINGS'] array is still supported. Third-party additions are automatically migrated to TCA after all ext_tables.php files have been loaded. However, this approach is deprecated, and extensions should migrate to the new TCA-based API.

Feature: #108846 - Console command to inspect global ViewHelper namespaces 

See forge#108846

Description 

The new console command typo3 fluid:namespaces has been introduced. It lists all the available global ViewHelper namespaces in the current project and can be used to verify the current configuration. The --json option can be used to access the information in a machine-readable way.

Usage:

vendor/bin/typo3 fluid:namespaces
Copied!

Example output:

+--------+------------------------------+
| Alias  | Namespace(s)                 |
+--------+------------------------------+
| core   | TYPO3\CMS\Core\ViewHelpers   |
+--------+------------------------------+
| formvh | TYPO3\CMS\Form\ViewHelpers   |
+--------+------------------------------+
| f      | TYPO3Fluid\Fluid\ViewHelpers |
|        | TYPO3\CMS\Fluid\ViewHelpers  |
+--------+------------------------------+
Copied!

The same information is available in the System > Configuration module in the TYPO3 backend.

Impact 

The new console command allows developers and integrators to inspect global ViewHelper namespaces in the current project.

Feature: #108868 - Introduce Fluid f:render.text ViewHelper 

See forge#108868

Description 

A new <f:render.text> ViewHelper has been added. It provides a consistent approach for outputting field values in templates where the field is part of a record.

The ViewHelper follows the same conventions as other rendering-related ViewHelpers and can be used wherever a text-based database field needs to be displayed in the frontend.

The ViewHelper is record-aware. It receives the full record and field name, and renders the field according to the field's TCA configuration. This includes both plain text and rich text fields.

By default, accessing a field that is not available in a record raises an exception. In order to support shared templates that need to be rendered even if a field is missing, the optional boolean argument optional can be set to true . The ViewHelper will then return null instead.

The input can be a RecordInterface , PageInformation , or a DomainObjectInterface .

This means records, ContentBlockData objects, PageInformation objects, and Extbase models can be input. PageInformation objects and Extbase models are converted internally to a RecordInterface.

Usage 

Usage with the record-transformation data processor:

dataProcessing {
    10 = record-transformation
}
Copied!

Based on the field's TCA configuration of the record in question, the ViewHelper chooses the appropriate processing of the field (plain text, multiline text, or rich text) without needing further configuration in the template.

MyContentElement.fluid.html
<f:render.text record="{record}" field="title" />
or
<f:render.text field="title">{record}</f:render.text>
or
{f:render.text(record: record, field: 'title')}
or
{record -> f:render.text(field: 'title')}
Copied!

Usage with optional fields:

SharedHeader.fluid.html
<f:variable name="header">{record -> f:render.text(field: 'header', optional: true)}</f:variable>
Copied!

This is useful for shared partials, for example in fluid_styled_content . A header partial can be reused by content elements whose transformed record does not provide a header or subheader field. Without optional="true" , rendering such a partial would raise a RecordPropertyNotFoundException . With optional="true" , the ViewHelper returns null and the partial can continue to handle the missing value gracefully.

Usage with an Extbase model (property name differs from database field name):

The field argument always refers to the database or TCA column name of the underlying record, even if your Extbase model maps that column to a differently named property.

Note that Extbase models need to contain all columns to be rendered and the record type column (if configured in TCA) for this to work correctly. For example, an Extbase model that represents tt_content must map both bodytext and CType to be able to use <f:render.text record="{contentModel}" field="bodytext" /> .

Blog/Templates/Post/Show.fluid.html
<f:render.text record="{post}" field="short_description" />

<!-- Example: Post->shortDescription maps to DB field "short_description";
     use field="short_description" here. -->
Copied!

Previously, you needed to choose different processing for plain text and rich text fields. You can now use the same ViewHelper for all field types.

For reference, similar results could previously be achieved using:

MyContentElement.fluid.html
{record.title}
Copied!

or multiline text:

MyContentElement.fluid.html
<f:format.nl2br>{record.description}</f:format.nl2br>
or
{record.description -> f:format.nl2br()}
Copied!

or, for rich text:

MyContentElement.fluid.html
<f:format.html>{record.bodytext}</f:format.html>
or
{record.bodytext -> f:format.html()}
Copied!

Migration 

Extensions that previously accessed field values with {record.title} can continue to do so. However, using <f:render.text> is recommended instead because it renders the field in the context of the record and applies processing based on the field configuration.

When migrating from formatting ViewHelpers like <f:format.nl2br> or <f:format.html> to <f:render.text> , the main difference is that the new ViewHelper is aware of the record it belongs to and renders the field based on the record's TCA schema.

If a template intentionally accesses fields that might not be available in every record, for example shared fluid_styled_content header partials used by custom content elements that do not have a visible header field, use the optional argument to preserve the previous behavior of treating the missing field as empty output.

Impact 

Theme creators are encouraged to use the <f:render.text> ViewHelper for rendering text-based fields (plain text and rich text) as it provides a standardized, record-aware approach that can be built upon in future versions.

The ViewHelper takes both the record and the field name as arguments so the rendering process has access to the complete record context. This makes the ViewHelper more flexible than directly accessing the field value.

Feature: #108904 - Add generic error action for custom HTTP status codes in ErrorController 

See forge#108904

Description 

The \TYPO3\CMS\Frontend\Controller\ErrorController has been enhanced with a new method customErrorAction() which allows custom error handling for HTTP status codes.

The new method can be used with TYPO3 site error handling, allowing site administrators to configure dedicated error handling (for example, rendering a Fluid template) for a given status code.

Example of usage in an Extbase action:

use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;

$response = GeneralUtility::makeInstance(ErrorController::class)->customErrorAction(
    $this->request,
    429,
    'Rate limit exceeded.',
    'You have exceeded the rate limit.'
);
throw new PropagateResponseException($response, 1771065101);
Copied!

Impact 

It is now possible to trigger custom error pages with specific HTTP status codes and messages from within TYPO3 or extensions, while still respecting the error handling in the main site configuration.

Feature: #108915 - New page creation wizard 

See forge#108915

Description 

The TYPO3 backend now features a new guided "Page Creation Wizard" designed to streamline the page creation process. This interface replaces the traditional, technically complex workflow with a modular and accessible step-by-step process.

The primary goals of the wizard are to ensure data integrity by enforcing mandatory fields during creation which improves accessibility, and provides a modern, responsive user experience that does not require deep TYPO3-specific expertise.

Key features 

  • Guided workflow: A step-by-step process including positioning, type selection, data entry, and a final review before persistence.
  • Data integrity: Validation of required fields (for example, page title) occurs at each step to prevent broken or incomplete page records.
  • Context-aware: The wizard can be triggered from various entry points (for example, page tree, Content > Records module) and respects predefined parameters like position and page type.
  • Modular and extensible: Built using a generic architecture that allows integrators to add custom steps or extend existing configuration for specific page types.
  • FormEngine integration: Dynamic steps are rendered using FormEngine, ensuring that all TCA-based rules and field configurations are respected.
  • Post-creation actions: After successful creation, users can choose whether to jump to the Content > Layout module, create another page, or return to their previous task.

Impact 

Editors benefit from a faster, less error-prone way to build page structures. The intuitive interface significantly lowers the barrier to entry for new users while maintaining the flexibility required by power users.

Developers and integrators can leverage the modular design to customize the creation process for custom doktype values or even adapt the wizard concept for other TYPO3 workflows in the future.

Feature: #108941 - Provide language labels as virtual JavaScript modules 

See forge#108941

Description 

JavaScript modules can now import language labels as code. The labels are exposed as an object with a get() method and allows placeholder substitution conforming to the ICU message format.

// Import labels from language domain "core.bookmarks"
import { html } from 'lit';
import labels from '~labels/core.bookmarks';

// Use label
html`<p>{labels.get('groupType.global')}</p>`

// Retrieve label and use ICU MessageFormat placeholders
// Example label: <source>File "{filename}" deleted</source>
html`<p>{labels.get('file.deleted', { filename: 'my-file.txt' })}</p>`

// Render a label containing pseudo XML tags
// Example label: "File <bold>{filename}</bold> deleted"
html`<p>{labels.get('file.deleted', {
    filename: 'my-file.txt',
    // Callback function that renders the contents of <bold>
    bold: chunks => html`<strong>${chunks}</strong>`,
})}</p>`
Copied!

This means controllers do not need to inject arbitrary labels into the global TYPO3.lang configuration, which impeded writing generic web components.

Virtual JavaScript modules (schema ~labels/{language.domain}) are created that resolve the labels for the specified language domain provided after the ~labels/ prefix. This mapping is implemented technically by using an import map path prefix which instructs the JavaScript engine to append a specified suffix to the mapped prefix.

The labels can be cached client-side with a far-future cache lifetime, similar to static resources. TYPO3 therefore generates version-specific and locale-specific URLs to ensure labels can be cached by the user agent without requiring explicit cache invalidation.

Impact 

Extension developers can now use labels in JavaScript components without requiring labels to be preloaded globally or per module, reducing the risk of missing labels and also simplifying developer workflows.

Workarounds such as pushing labels to the top frame, loading labels globally, and adding labels to component attributes have previously been used and are replaced by this infrastructure.

Feature: #108966 - Rich text editor support in TYPO3 form editor 

See forge#108966

Description 

The TYPO3 form editor now supports rich text editing for textarea fields with CKEditor 5. Form elements can be configured to use any available RTE preset, which provides a consistent editing experience across the TYPO3 backend.

The implementation includes a new \TYPO3\CMS\Form\Service\RichTextConfigurationService that resolves CKEditor configuration from global TYPO3 RTE presets and prepares it for use in the form editor context. External plugins, such as the TYPO3 link browser, are configured automatically.

Impact 

Form integrators can now enable rich text editing in any textarea field in the form editor by configuring it in the form YAML configuration.

The following form elements and finishers now support rich text editing out of the box:

  • StaticText element - formatted text in forms
  • Checkbox element - labels with links for privacy policies, etc.
  • Confirmation finisher - formatted confirmation messages

All textarea fields in custom form elements can be configured to use the RTE.

Basic configuration 

Enable rich text editing for a form element:

EXT:my_extension/Configuration/Form/MyFormSetup.yaml
prototypes:
  standard:
    formElementsDefinition:
      StaticText:
        formEditor:
          editors:
            300:
              identifier: staticText
              templateName: Inspector-TextareaEditor
              label: formEditor.elements.StaticText.editor.staticText.label
              propertyPath: properties.text
              enableRichtext: true
              richtextConfiguration: form-label
Copied!

The richtextConfiguration option accepts any registered RTE preset name, for example:

  • form-label - simple formatting for labels (bold, italic, link) - default
  • form-content - extended formatting for content fields (includes lists)
  • default - standard TYPO3 RTE with all features
  • minimal - minimal feature set

New form RTE presets 

Two new RTE presets specifically designed for the form extension are now available:

form-label
Essential formatting options for labels and short text fields. Includes: bold, italic, link
form-content
Extended formatting options for content fields like StaticText. Includes: bold, italic, link, bulleted lists, numbered lists

Configuration options 

The following options are available for textarea editors in the form editor:

enableRichtext
Data type
boolean
Default
false
Description
Enables rich text editing for this textarea field.
richtextConfiguration
Data type
string
Default
form-label
Description
Name of the RTE preset to use. The preset must be registered in $GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'] . Common presets: form-label, form-content, default, minimal, full

Custom sanitizer configuration 

The form extension uses a multi-layer sanitization approach for security:

  • Backend: Content is sanitized using the htmlSanitize.build setting from the RTE preset processing configuration.
  • Frontend: Content is sanitized again using the default sanitizer via the f:sanitize.html() ViewHelper.

To use a custom sanitizer in the backend, configure it in your RTE preset:

EXT:my_extension/Configuration/RTE/MyPreset.yaml
processing:
  HTMLparser_db:
    htmlSanitize:
      build: \MyVendor\MyExtension\Html\MySanitizerBuilder
Copied!

Frontend customization 

The frontend templates use f:sanitize.html() with the default sanitizer for defense-in-depth security. To customize the frontend sanitization, integrators have two options:

Option 1: Override Fluid templates

Override the form element templates and specify a custom sanitizer build:

EXT:my_extension/Resources/Private/Frontend/Partials/StaticText.html
{formvh:translateElementProperty(element: element, property: 'text')
    -> f:sanitize.html(build: 'myCustomBuild')
    -> f:transform.html()}
Copied!

Option 2: Register a custom default sanitizer

Register a custom sanitizer builder as the default sanitizer globally:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer']['default']
    = \MyVendor\MyExtension\Html\MySanitizerBuilder::class;
Copied!

Feature: #108975 - Add configuration provider for Extbase class configuration 

See forge#108975

Description 

Extbase class configuration (persistence mapping) is now exposed in the backend System > Configuration module. The module is available if the system extension typo3/cms-lowlevel is installed.

The displayed configuration reflects the configured mapping that Extbase uses at runtime. It is built by collecting and merging all EXT:my_extension/Configuration/Extbase/Persistence/Classes.php definitions from active packages.

Impact 

This is a read-only usability improvement. Developers and integrators can inspect and verify resolved Extbase persistence class mapping such as extension overrides in the backend, without having to dump configuration arrays or manually check each EXT:my_extension/Configuration/Extbase/Persistence/Classes.php file.

Feature: #108982 - Introduce rate limiting for Extbase actions 

See forge#108982

Description 

Extbase now supports rate limiting for controller actions using the new PHP attribute \TYPO3\CMS\Extbase\Attribute\RateLimit . This feature allows developers to restrict the number of requests a user can make to a specific action within a given time frame.

Rate limiting is based on the client's IP address and uses Symfony's RateLimiter component with caching framework storage.

The #[RateLimit] attribute supports the following properties:

  • limit : The maximum number of requests allowed (default: 5).
  • interval : The time window for the limit (for example, 15 minutes, 1 hour) (default: 15 minutes).
  • policy : The rate limiting policy to use (for example, sliding_window, fixed_window) (default: sliding_window).
  • message : An optional, localizable translation key for the error message shown when the limit is reached, for example messages.rate_limit_message (the translation domain, such as my_extension, is added automatically and must not be part of the key), or LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:rate_limit_message.

When a rate limit is exceeded, Extbase returns a response with HTTP status code 429 by default (Too Many Requests).

Usage 

Apply a rate limit to an Extbase action by adding a #[RateLimit] attribute to the action method:

EXT:my_extension/Classes/Controller/MyController.php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\RateLimit;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[RateLimit(limit: 3, interval: '1 minute', message: 'message.ratelimitexceeded')]
    public function createAction(): ResponseInterface
    {
        // Business logic for creating an entity
        return $this->redirect('index');
    }
}
Copied!

PSR-14 event: BeforeActionRateLimitResponseEvent 

The new PSR-14 event \TYPO3\CMS\Extbase\Event\BeforeActionRateLimitResponseEvent is dispatched when a rate limit is triggered but before the response is returned. This allows extension developers to modify the response or perform additional actions, such as logging, throwing a custom exception, and enqueuing a flash message.

The following example implementation shows how to throw a custom error if a rate limit is reached. It is handled by a configured site error handler.

EXT:my_extension/Classes/EventListener/ModifyRateLimitResponse.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Event\BeforeActionRateLimitResponseEvent;
use TYPO3\CMS\Frontend\Controller\ErrorController;

final readonly class MyEventListener
{
    #[AsEventListener('my_extension/modify-rate-limit-response')]
    public function __invoke(BeforeActionRateLimitResponseEvent $event): void
    {
        $response = GeneralUtility::makeInstance(ErrorController::class)
            ->accessDeniedAction(
                $event->getRequest(),
                $event->getRateLimit()->message,
            );
        throw new PropagateResponseException($response, 1771077885);
    }
}
Copied!

Impact 

Developers can now protect sensitive Extbase actions (for example, form submissions, login attempts, and heavy API endpoints) from abuse, spam, or brute-force attacks with minimal effort.

Feature: #108992 - New PSR-14 event for workspace dependency resolution 

See forge#108992

Description 

A new PSR-14 event \TYPO3\CMS\Workspaces\Event\IsReferenceConsideredForDependencyEvent has been added. It is dispatched for each sys_refindex row when the workspace dependency resolver evaluates which references constitute structural dependencies during publish, stage, discard, and display operations.

Listeners decide whether a particular reference should be treated as a workspace dependency. References are opt-in: the default is "not a dependency", and listeners must explicitly mark relevant references.

The event has the following methods:

  • getTableName() : The table owning the field ( sys_refindex.tablename ).
  • getRecordId() : The record owning the field ( sys_refindex.recuid ).
  • getFieldName() : The TCA field name ( sys_refindex.field ).
  • getReferenceTable() : The referenced table ( sys_refindex.ref_table ).
  • getReferenceId() : The referenced record ID ( sys_refindex.ref_uid ).
  • getAction() : The \TYPO3\CMS\Workspaces\Dependency\DependencyCollectionAction enum value ( Publish , StageChange , Discard , or Display ).
  • getWorkspaceId() : The current workspace ID.
  • isDependency() / setDependency() : Read or change whether this reference is a structural dependency.

TYPO3 Core registers a listener that marks type=inline , type=file (with foreign_field ), and type=flex fields as dependencies.

A new enum \TYPO3\CMS\Workspaces\Dependency\DependencyCollectionAction has been added to represent the action context.

Example 

A third-party extension that stores parent-child relationships in a custom field can register a listener to include those references as workspace dependencies:

EXT:my_extension/Classes/EventListener/WorkspaceDependencyListener.php
namespace Vendor\MyPackage\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Workspaces\Event\
    IsReferenceConsideredForDependencyEvent;

#[AsEventListener('my-package/workspace-dependency')]
final class WorkspaceDependencyListener
{
    public function __invoke(
        IsReferenceConsideredForDependencyEvent $event
    ): void {
        if ($event->getFieldName() === 'tx_mypackage_parent') {
            $event->setDependency(true);
        }
    }
}
Copied!

Impact 

Extensions can now register custom parent-child relationships as workspace dependencies via this PSR-14 event. This ensures that structurally dependent records are published, staged, or discarded together, preventing orphaned records in workspaces.

The internal pseudo-event mechanism (EventCallback, ElementEntityProcessor) that was previously used has been removed. This is an internal change that does not affect the public API.

Feature: #109018 - PSR-14 event to modify indexed_search result sets 

See forge#109018

Description 

A new PSR-14 event \TYPO3\CMS\IndexedSearch\Event\AfterSearchResultSetsAreGeneratedEvent has been introduced to modify search result sets in SearchController .

The event is dispatched in searchAction() after all the result sets have been built. Event listeners can manipulate complete result sets, including pagination, rows, section data, and category metadata.

The event has the following methods:

  • getResultSets() : Returns all the result sets from the current search.
  • setResultSets(array $resultSets) : Replaces the result sets.
  • getSearchData() : Returns the search configuration array.
  • getSearchWords() : Returns an array of search words.
  • getView() : Returns the view instance.
  • getRequest() : Returns the current server request.

Example 

The following example replaces every result set pagination with SlidingWindowPagination :

EXT:my_extension/Classes/EventListener/ModifySearchPaginationListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Core\Pagination\SlidingWindowPagination;
use TYPO3\CMS\IndexedSearch\Event\AfterSearchResultSetsAreGeneratedEvent;

#[AsEventListener(identifier: 'my-extension/modify-search-result-sets')]
final readonly class ModifySearchPaginationListener
{
    public function __invoke(
        AfterSearchResultSetsAreGeneratedEvent $event
    ): void {
        $resultSets = $event->getResultSets();

        foreach ($resultSets as $key => $resultSet) {
            if (($resultSet['pagination'] ?? null)
                instanceof SimplePagination
            ) {
                $resultSets[$key]['pagination']
                    = new SlidingWindowPagination(
                        $resultSet['pagination']->getPaginator(),
                        5
                    );
            }
        }

        $event->setResultSets($resultSets);
    }
}
Copied!

Impact 

This event allows search result sets to be modified in a single listener call. It enables custom pagination strategies, as well as advanced search result transformations.

Feature: #109031 - Page position select component 

See forge#109031

Description 

A new component has been added, based on the page-browser, that allows a page to be selected and an insertion position to be defined. Possible positions are inside and after.

Features 

  • When a page node in the tree is selected insertion options are displayed. Options include Insert and After. After is applicable to all child pages.
  • On first render, the selected node is scrolled into view using scrollNodeIntoViewIfNeeded.
  • The component emits a custom event typo3:page-position-select-tree:insert-position-change whenever the insertion position changes. The event payload contains pageUid (the selected page ID) and position (the chosen insertion position), allowing other modules to react accordingly.

Example usage 

<typo3-backend-component-page-position-select
    activePageId="1"
    insertPosition="inside"
>
</typo3-backend-component-page-position-select>
Copied!

Impact 

This component can be used anywhere in the backend where page selection and insertion position are needed, replacing previous workflows with more intuitive controls.

Feature: #109080 - Unified RateLimiterFactory with admin overrides 

See forge#109080

Description 

TYPO3's \TYPO3\CMS\Core\RateLimiter\RateLimiterFactory has been refactored to serve as the single entry point for rate limiting across the system. A new \TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface extends Symfony's RateLimiterFactoryInterface with additional convenience methods for request-based and login rate limiting.

Previously, backend and frontend password recovery features and Extbase rate limiting each created Symfony rate limiter factories, bypassing TYPO3's factory. All consumers now use the central TYPO3 factory, which enables a unified admin override mechanism.

Extension developers should type-hint against RateLimiterFactoryInterface when injecting the factory.

Admin overrides via TYPO3_CONF_VARS 

A new configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter'] allows administrators to override any rate limiter's settings by its ID. Each key is a limiter ID, and each value is an array of settings to override:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['login-be'] = [
    'limit' => 3,
    'interval' => '5 minutes',
];

$GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['backend-password-recovery'] = [
    'limit' => 1,
    'interval' => '1 hour',
];
Copied!

Known limiter IDs:

  • login-be — backend login
  • login-fe — frontend login
  • backend-password-recovery — backend password reset
  • felogin-password-recovery — frontend password recovery
  • extbase-<classSlug>-<actionMethod> — Extbase #[RateLimit] actions

Example limiter ID for Extbase action 

The limiter ID for an Extbase action with the #[RateLimit] attribute is constructed using the "slugified" class name and the action method name.

EXT:my_extension/Classes/Controller/MyController.php
namespace Vendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\RateLimit;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[RateLimit(
        limit: 5,
        interval: '10 minutes',
        message: 'ratelimit.dosomething',
    )]
    public function doSomethingAction(): ResponseInterface
    {
        return $this->redirect('index');
    }
}
Copied!

The limiter ID for the action is extbase-vendor-myextension-controller-mycontroller-dosomethingaction

General-purpose rate limiting 

Extension developers can now use the factory for custom rate limiting needs.

The createRequestBasedLimiter() method is the recommended entry point for request-scoped rate limiting. It extracts the client's remote IP from the PSR-7 request and uses it as the limiter key:

EXT:my_extension/Classes/Service/MyService.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;

class MyService
{
    public function __construct(
        private readonly RateLimiterFactoryInterface $rateLimiterFactory,
    ) {}

    public function doSomething(ServerRequestInterface $request): void
    {
        $limiter = $this->rateLimiterFactory->createRequestBasedLimiter(
            $request,
            [
                'id' => 'my-extension-action',
                'policy' => 'sliding_window',
                'limit' => 10,
                'interval' => '1 hour',
            ],
        );

        $limit = $limiter->consume();
        if (!$limit->isAccepted()) {
            // Handle rate limit exceeded
        }
    }
}
Copied!

In cases where a custom key is needed, for example a user ID instead of the IP address, the createLimiter() method accepts an explicit configuration array and key:

$limiter = $this->rateLimiterFactory->createLimiter(
    [
        'id' => 'my-extension-action',
        'policy' => 'sliding_window',
        'limit' => 10,
        'interval' => '1 hour',
    ],
    $userId,
);
Copied!

Preconfigured named services can also be defined in Services.yaml. They are then injectable with the create() method from the RateLimiterFactoryInterface :

EXT:my_extension/Configuration/Services.yaml
myRateLimiter:
  class: TYPO3\CMS\Core\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'my-custom-limiter'
      policy: 'sliding_window'
      limit: 5
      interval: '10 minutes'
Copied!

Impact 

All rate limiting in TYPO3 now flows through a single factory that supports admin-level overrides. Administrators can tune or restrict rate limits for any component—login, password recovery, Extbase actions, or custom extensions— without modifying code, using $GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter'] .

The login rate limiter now uses human-readable IDs (login-be, login-fe) instead of SHA1 hashes. Existing cached rate limit state from previous versions will expire naturally.

Feature: #109087 - Introduce BeforeBackendPageRenderEvent for BackendController 

See forge#109087

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent has been introduced. It is dispatched in BackendController before the main backend page is rendered. It provides access to:

  • $view ( ViewInterface ) – assign template variables to the backend top frame view
  • $javaScriptRenderer ( JavaScriptRenderer ) – add custom JavaScript modules to the backend top frame
  • $pageRenderer ( PageRenderer ) – add assets such as CSS files (marked @internal )

Example 

EXT:my_extension/Classes/EventListener/BeforeBackendPageRenderEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

#[AsEventListener(identifier: 'my-extension/before-backend-page-render')]
final class BeforeBackendPageRenderEventListener
{
    public function __invoke(BeforeBackendPageRenderEvent $event): void
    {
        $event->javaScriptRenderer->addJavaScriptModuleInstruction(
            JavaScriptModuleInstruction::create(
                '@my-vendor/my-extension/backend-module.js'
            )
        );
    }
}
Copied!

Impact 

It is now possible to add custom JavaScript modules and other assets to the TYPO3 backend top frame using the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent .

Feature: #109110 - Introduce scheduler task priority 

See forge#109110

Description 

A new priority column has been added to the tx_scheduler_task table, allowing administrators to control the execution order of scheduler tasks. Three levels are available:

  • High (150)
  • Regular (100, default)
  • Low (50)

The scheduler now selects the next executable task ordered by priority DESC first, using nextexecution ASC as a secondary tiebreaker. This means a high-priority task is always executed before a lower-priority task, regardless of how long the lower-priority task has been waiting.

The priority field is exposed as a select field in the Timing tab of the task editing form in all registered task types. The priority of each task is also visible in the scheduler backend module list view.

Extending priority levels 

Extensions can add custom priority levels by extending the TCA of tx_scheduler_task . The priority field is a plain integer column, so any positive integer value is valid. The scheduler module automatically resolves the label of any registered TCA item, so that custom values are displayed correctly in the list view. The TCA item's label key must point to a valid language label. If no matching item is found, the raw integer is shown.

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTcaSelectItem(
    'tx_scheduler_task',
    'priority',
    [
        'label' => 'LLL:my_extension.messages:priority.critical',
        'value' => 200,
    ],
    150,
    'after',
);
Copied!

Choose integer values that fit naturally into the existing scale (50 / 100 / 150). Values above 150 are executed before High, values below 50 after Low.

Impact 

Administrators can now assign a priority to each scheduler task. Tasks with High priority are picked up before Regular tasks, and Regular before Low tasks. If multiple tasks share the same priority, the longest-overdue task is still selected first, preserving the previous behavior as a tiebreaker.

Existing tasks receive the default priority Regular (100) automatically via the schema update — no data migration is required.

Feature: #109114 - Autocomplete for components via XSD schema 

See forge#109114

Description 

The existing CLI command typo3 fluid:schema:generate has been extended to cover Fluid components. When executed, the command creates *.xsd files in var/transient/ for all available ViewHelpers and components, which can be used by IDEs for autocompletion.

Usage:

vendor/bin/typo3 fluid:schema:generate
Copied!

In order to work correctly the responsible component collection needs to implement the new ComponentListProviderInterface . TYPO3's Fluid components integration already implements this, so these components are supported out of the box.

Fluid Standalone has a default implementation of custom component collections that are based on AbstractComponentCollection , which should cover components that were created before the official components integration (such as those created with TYPO3 v13). However, if a custom folder structure is used by overriding the default resolveTemplateName() , a custom implementation of getAvailableComponents() must be provided. In most cases, it is easier to switch to the TYPO3 integration and remove the custom class.

Impact 

The CLI command typo3 fluid:schema:generate now creates XSD schema files for Fluid components, enabling autocompletion in supporting IDEs.

Feature: #109126 - Introduce date editor for ext:form 

See forge#109126

Description 

A new web component <typo3-form--date-editor> has been introduced in the form editor backend. It replaces the plain text input in the DateRange validator minimum/maximum fields and the defaultValue field in the Date form element.

Previously editors had to manually type date values or relative expressions like -18 years into a plain text field. The new structured editor provides a user-friendly UI which has five modes:

  • No value - Clears the constraint (empty value)
  • Today - Sets the value to today
  • Absolute date - A native HTML5 date picker that produces Y-m-d values
  • Relative date - Structured input with direction (past/future), amount and unit (days, weeks, months, years), which produces expressions like -18 years or +1 month
  • Custom relative expression - Free-text input for arbitrary relative date expressions that go beyond the structured input, such as compound expressions like +1 month +3 days . The input is validated against the configured relative date pattern.

Impact 

The form editor backend now provides a structured, user-friendly editor for date constraints and default values in Date form elements. Editors no longer need to know the PHP relative date syntax - they can simply select a mode, direction, amount, and unit from dropdown fields. For advanced use cases, the custom mode allows arbitrary relative date expressions with real-time validation to be entered. Existing form definitions are not affected and continue to work without change.

Feature: #109130 - Context-aware editing in the layout module 

See forge#109130

Description 

The Content > Layout module now features a context panel for editing page properties and content elements. Clicking on an edit button opens a slide-in panel next to the page layout. The editing form is displayed inside the panel with the page layout remaining visible in the background.

The panel supports all FormEngine fields in an improved UI. The panel header displays the record title as well as Save and Close buttons. An Expand button allows switching to the full record editing form in the content area at any time. After saving, the panel stays open for further edits.

User settings 

The context panel is enabled by default. It can be disabled per user in User Settings via the Use quick editing for records in the page module option. When disabled, edit buttons navigate to the full record editing form as before. The setting takes effect immediately.

Impact 

Editors can now edit records in the Content > Layout module without leaving the page layout context. The full editing form remains accessible for more complex editing tasks.

Feature: #109163 - Implement public system resources publishing 

See forge#109163

Description 

When implementing the new system resources API (Feature: #107537 - System resource API for system file access and public URI generation), resource publishing was skipped and has now been implemented.

The most visible feature of this implementation is the new asset:publish command. This command can publish public extension resources from their Resources/Public folder to the document root directory (public by default in Composer mode).

To maintain backward compatibility for Composer mode installations, this command is automatically executed during composer install. This means that after Composer has done its job installing packages, extension assets are already published.

Public extension resources are also published when extensions are set up with the extension:setup command or when an extension is activated in the Extension Manager. Because of this, and because it might not be desirable or applicable to publish assets at Composer build time, it is now possible to skip publishing during composer install by setting an environment variable TYPO3_SKIP_ASSET_PUBLISH, for example: TYPO3_SKIP_ASSET_PUBLISH=1 composer install. Not publishing assets at composer install is likely to become default behavior in future TYPO3 versions.

TYPO3 ships file system-based publishing only. From now on, however, there is an additional strategy available besides symlink publishing (*nix systems) and junction publishing (Windows systems). TYPO3 can now copy all files and folders from their private locations to the document root. This is useful for many use cases such as container builds, deployments with read-only file systems, restrictive hosting environments, and others.

By default, the linking strategy is being kept, particularly for backward compatibility reasons. It is, however, possible to influence the behavior by setting the following configuration option:

Default behavior: always link:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['SystemResources']['filesystemPublishingType'] = 'link';

Always copy / mirror files:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['SystemResources']['filesystemPublishingType'] = 'mirror';

Copy / mirror files in a Production context and link folders in a Development context:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['SystemResources']['filesystemPublishingType'] = 'auto';

Beyond file system publishing 

Although TYPO3 Core only delivers file system-based publishing, third-party extensions can now implement other ways of publishing public system resources.

By implementing \TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface and registering the implementing class as an alias of the interface, TYPO3 will use this not only to publish system resources, but also to generate URIs that reflect their new location, for example on a CDN.

This also works in TYPO3 classic mode, because publishing is now part of extension activation.

Simple example of how to generate URIs for a CDN:

EXT:my_extension/Classes/Service/ExampleResourcePublisher.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\DefaultSystemResourcePublisher;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
use TYPO3\CMS\Core\SystemResource\Type\PublicPackageFile;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;

#[Autoconfigure(public: true), AsAlias(SystemResourcePublisherInterface::class, public: true)]
final readonly class ExampleResourcePublisher implements SystemResourcePublisherInterface
{
    private const CDN_URL = 'https://my.awsome.cdn/files/';

    public function __construct(
        private DefaultSystemResourcePublisher
            $defaultSystemResourcePublisher,
    ) {}

    public function publishResources(
        PackageInterface $package,
    ): FlashMessageQueue {
        // Additional logic to publish files to a CDN could be added
        // here. For this example, the CDN loads the assets from the
        // source automatically, so resources are published as usual.
        return $this->defaultSystemResourcePublisher->publishResources(
            $package,
        );
    }

    public function generateUri(
        PublicResourceInterface $publicResource,
        ?ServerRequestInterface $request,
        ?UriGenerationOptions $options = null,
    ): UriInterface {
        $defaultUri = $this->defaultSystemResourcePublisher->generateUri(
            $publicResource,
            $request,
            new UriGenerationOptions(
                uriPrefix: '',
                absoluteUri: false,
                cacheBusting: false,
            ),
        );
        if ($publicResource instanceof PublicPackageFile) {
            return new Uri(self::CDN_URL . $defaultUri);
        }
        return $defaultUri;
    }
}
Copied!

Impact 

There is no apparent impact for any TYPO3 installation, as the changes are mostly internal and the public API and behavior are the same as before. For deployments, nothing needs to be changed, as asset publishing is still performed at composer install, and also by the extension:setup command, both of which are already part of any deployment workflow.

Users, however, now have more control over when and how publishing is performed, by setting the environment variable TYPO3_SKIP_ASSET_PUBLISH=1 for composer install or by configuring the mirror strategy for publishing by setting $GLOBALS['TYPO3_CONF_VARS']['SYS']['SystemResources']['filesystemPublishingType'] = 'mirror'; in config/system/additional.php.

Feature: #109167 - Improved exceptions in Fluid templates 

See forge#109167

Description 

In an effort to simplify debugging Fluid templates, TYPO3 14 enhances exception messages thrown by Fluid in several ways:

  • Templates that contain invalid syntax or refer to undeclared ViewHelper arguments now contain both the full path to the template file and the affected line number in that file.
  • Most ViewHelper-related error messages now contain the full path to the template file.
  • Fluid Standalone 5.2 (also backported to Fluid 4.6) introduces more granular exception classes that can be used by ViewHelpers to classify runtime errors. These classifications are also part of the error message.
  • When a referenced Fluid template cannot be found, the exception message contains a full list of the candidates that have been tried. Also, the exception contains the context in which the template file is missing (for example FLUIDTEMPLATE or PAGEVIEW).

In order for this to work with custom ViewHelper implementations, ViewHelpers need to use the base ViewHelper exception class or one of its child classes:

  • \TYPO3Fluid\Fluid\Core\ViewHelper\Exception for general exceptions
  • \TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentException for general exceptions related to ViewHelper arguments
  • \TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException for invalid ViewHelper argument values (e.g. wrong type, empty, invalid format)
  • \TYPO3Fluid\Fluid\Core\ViewHelper\MissingArgumentException if a required ViewHelper argument has not been supplied
  • \TYPO3Fluid\Fluid\Core\ViewHelper\UndeclaredArgumentException if a ViewHelper is called with an argument that has not been defined

If any of these exception classes are used in a ViewHelper, Fluid's internal error handler automatically adds the full path to the current template file to the exception. It is not necessary for ViewHelpers to do this themselves. Note that this leads to nested exceptions. The original exception can be accessed via $e->getPrevious() .

Examples 

Parse error in template
#1238169398 TYPO3Fluid\Fluid\Core\Parser\Exception
Fluid parse error in template /var/www/html/typo3conf/ext/theme/Resources/Private/Components/Test/Test.fluid.html, line 11 at character 15.
Error: Not all tags were closed! (error code 1238169398). Template source chunk: test
Copied!
Undeclared ViewHelper argument
#1773227091 TYPO3Fluid\Fluid\Core\ViewHelper\Exception
TYPO3Fluid\Fluid\Core\ViewHelper\UndeclaredArgumentException in /var/www/html/typo3conf/ext/theme/Resources/Private/Components/Test/Test.fluid.html:
Undeclared arguments passed to ViewHelper TYPO3Fluid\Fluid\ViewHelpers\Format\TrimViewHelper: foo. Valid arguments are: value, characters, side
(/var/www/html/vendor/typo3fluid/fluid/src/Core/ViewHelper/AbstractViewHelper.php:314)
Copied!
Custom validation by ViewHelper implementation
#1669191560 TYPO3Fluid\Fluid\Core\ViewHelper\Exception
TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException in /var/www/html/typo3conf/ext/theme/Resources/Private/Components/Test/Test.fluid.html:
The side "none" supplied to Fluid's format.trim ViewHelper is not supported.
(/var/www/html/vendor/typo3fluid/fluid/src/ViewHelpers/Format/TrimViewHelper.php:118)
Copied!
Missing template file for PAGEVIEW
#1742058289 TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException
PAGEVIEW TypoScript object: Failed to resolve a template file for page layout "default". See also: https://docs.typo3.org/permalink/t3tsref:cobj-pageview@14.2.
The following paths were checked:
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/default.fluid.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/default.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/default",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/Default.fluid.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/Default.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default/Default",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/default.fluid.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/default.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/default",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default.fluid.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default.html",
"/var/www/html/typo3conf/ext/dummy/Resources/Private/Templates/Pages/Default"
Copied!

Impact 

To make debugging easier, exceptions that originate from Fluid templates now contain more context, such as the full path to the template file.

Feature: #109187 - Add integrity property to CSS includes 

See forge#109187

Description 

The TypoScript properties includeCSS and includeCSSLibs now support the integrity property for Subresource Integrity (SRI) checking, bringing CSS includes to parity with existing SRI support in includeJS , includeJSFooter , includeJSLibs , and includeJSFooterlibs .

The crossorigin property is also supported. When integrity is set without an explicit crossorigin value for URI resources (for example, external URLs), crossorigin="anonymous" is automatically added, which is required for SRI validation to work cross-origin.

The integrity attribute has not yet been added to inline styles ( inline = 1 ), as SRI only applies to external resources.

The PageRenderer::addCssFile() and PageRenderer::addCssLibrary() methods have gained two new parameters, $integrity and $crossorigin .

Impact 

It is now possible to add SRI integrity hashes to CSS files included via TypoScript:

page.includeCSS {
    main = https://cdn.example.com/styles/main.css
    main.integrity = sha384-abc123==
    # crossorigin is auto-set to "anonymous" when integrity is given
}

page.includeCSSLibs {
    vendor = https://cdn.example.com/vendor.css
    vendor.integrity = sha384-xyz789==
    vendor.crossorigin = anonymous
}
Copied!

This results in the following HTML output:

<link rel="stylesheet" href="https://cdn.example.com/styles/main.css" media="all" integrity="sha384-abc123==" crossorigin="anonymous">
Copied!

Feature: #109187 - Automatic SRI hash resolution for resource includes 

See forge#109187

Description 

Setting integrity = auto on any resource include that supports the integrity property causes TYPO3 to automatically compute and inject the Subresource Integrity (SRI) hash for that resource instead of requiring a manually precomputed hash value.

This works for all integrity-supporting TypoScript include properties:

  • page.includeCSS
  • page.includeCSSLibs
  • page.includeJS
  • page.includeJSLibs
  • page.includeJSFooter
  • page.includeJSFooterlibs

The hash is computed using SHA-256, and the result is cached via cache.assets with a 7-day TTL, so there is no per-request overhead after the first render of a given resource.

For external URL resources, crossorigin="anonymous" is added automatically after the hash is successfully resolved, as required by the SRI specification for cross-origin resources.

The equivalent PHP constant \TYPO3\CMS\Core\Page\ResourceHashCollection::AUTO can be used when calling PageRenderer or AssetCollector APIs.

Impact 

It is now possible to enable SRI for resource includes without manually computing the hash value. Setting integrity = auto is sufficient:

page.includeCSS {
    main = https://cdn.example.com/styles/main.css
    main.integrity = auto
    # crossorigin="anonymous" is added automatically for external URLs
}

page.includeJS {
    app = EXT:my_extension/Resources/Public/JavaScript/app.js
    app.integrity = auto
}
Copied!

This results in output such as:

<link rel="stylesheet" href="https://cdn.example.com/styles/main.css" media="all" integrity="sha256-abc123==" crossorigin="anonymous">
<script src="/typo3conf/ext/my_extension/Resources/Public/JavaScript/app.js" integrity="sha256-xyz789=="></script>
Copied!

When using the PHP API directly, pass ResourceHashCollection::AUTO as the $integrity argument:

use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Page\ResourceHashCollection;

// Ignore all parameters but the last one ($integrity). They are defaults,
// but must be specified because TYPO3 Core API does not provide stable
// argument names, so using named arguments for regular methods is not supported.

$pageRenderer->addCssFile(
    'EXT:my_extension/Resources/Public/Css/style.css',
    'stylesheet',
    'all',
    '',
    null,
    false,
    '',
    null,
    '|',
    false,
    [],
    ResourceHashCollection::AUTO, // parameter $integrity
);

$pageRenderer->addJsFile(
    'EXT:my_extension/Resources/Public/JavaScript/app.js',
    '',
    null,
    false,
    '',
    null,
    '|',
    false,
    ResourceHashCollection::AUTO, // parameter $integrity
);

$assetCollector->addStyleSheet(
    'my-styles',
    'EXT:my_extension/Resources/Public/Css/style.css',
    [],
    ['integrity' => ResourceHashCollection::AUTO], // parameter $options
);
Copied!

Feature: #109214 - Native support for language Lao added 

See forge#109214

Description 

TYPO3 now supports Lao (sometimes referred to as "Laotian"). Lao is the official language of Laos.

The ISO 639-1 code for Lao is lo, which is how TYPO3 accesses the language internally.

Impact 

It is now possible to

  • fetch translated labels from Crowdin automatically inside the TYPO3 backend
  • switch the backend interface to Lao
  • create translation files with the lo prefix (such as lo.locallang.xlf) to define custom labels

TYPO3 will treat Lao like any other supported language.

Feature: #109263 - Expression language support for limitToPages in routing 

See forge#109263

Description 

The limitToPages option for route enhancers in the site configuration now supports Symfony Expression Language expressions in addition to plain page IDs.

Previously, limitToPages accepted only an array of integer page IDs, which required maintaining a static list that had to be updated whenever pages were added or moved. This change means string entries in the limitToPages array are evaluated as Expression Language expressions, giving integrators flexible, condition-based control over which pages a route enhancer applies to.

All entries in the array are combined via logical OR. Integer values are matched against the page ID (existing behavior), and string values are evaluated as expressions. To combine multiple conditions with logical AND, use the && operator inside a single expression string.

Use the following variables inside expressions:

  • page - The full page record as an associative array (for example, page["doktype"] , page["backend_layout"] , page["module"] )
  • site - The current Site object
  • siteLanguage - The current SiteLanguage object

All the default Expression Language functions such as like() , env() , and feature() are also available. Extensions can register additional functions and variables for the routing Expression Language context via Configuration/ExpressionLanguage.php.

Examples 

Match pages by their page type (doktype):

config/sites/<identifier>/config.yaml
routeEnhancers:
  NewsPlugin:
    type: Extbase
    limitToPages:
      - 'page["doktype"] == 1'
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/list/{page}'
        _controller: 'News::list'
      - routePath: '/detail/{news_title}'
        _controller: 'News::detail'
Copied!

Match pages by their backend layout:

config/sites/<identifier>/config.yaml
routeEnhancers:
  BlogPlugin:
    type: Extbase
    limitToPages:
      - 'page["backend_layout"] == "pagets__blog"'
    extension: Blog
    plugin: Posts
    routes:
      - routePath: '/post/{post_title}'
        _controller: 'Post::show'
Copied!

Combine integer page IDs with expression conditions (OR logic):

config/sites/<identifier>/config.yaml
routeEnhancers:
  ShopPlugin:
    type: Extbase
    limitToPages:
      - 42
      - 'page["module"] == "shop"'
    extension: Shop
    plugin: Products
    routes:
      - routePath: '/product/{product_title}'
        _controller: 'Product::show'
Copied!

Use AND logic inside a single expression:

config/sites/<identifier>/config.yaml
routeEnhancers:
  SpecialPlugin:
    type: Extbase
    limitToPages:
      - 'page["doktype"] == 1 && page["backend_layout"] == "pagets__special"'
    extension: MyExtension
    plugin: Special
    routes:
      - routePath: '/item/{item_title}'
        _controller: 'Item::show'
Copied!

Impact 

Integrators can now use dynamic, expression-based conditions in limitToPages instead of maintaining static lists of page IDs. This is fully backward compatible - existing configurations with integer-only arrays continue to work without any changes.

Feature: #109271 - Add TCA configuration for page creation wizard steps 

See forge#109271

Description 

The page creation wizard now supports the dynamic configuration of its steps via TCA. This allows integrators to define which fields are displayed in which step of the wizard, depending on the doktype.

A new TCA configuration option wizardSteps is introduced. It currently works only for the pages table. Each step is defined by a unique key and contains a title and a list of fields to be displayed.

The steps are sorted, allowing them to be positioned relative to each other using the after or before keys.

If a page type has required fields that are not explicitly assigned to a configured wizard step, a fallback step is automatically appended at the end.

Example 

Defining wizard steps for a custom doktype in TCA:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types']['123']['wizardSteps'] = [
    'setup' => [
        'title' => 'LLL:backend.wizards.page:step.setup',
        'fields' => ['title', 'slug', 'nav_title', 'hidden', 'nav_hide'],
    ],
    'special' => [
        'title' => 'LLL:my_extension.messages:wizard.special_step',
        'fields' => ['my_custom_field'],
        'after' => ['setup'],
    ],
];
Copied!

Impact 

It is now possible to configure fields, steps, and their order in the page creation wizard.

Feature: #109340 - Include site configurations in import / export 

See forge#109340

Description 

The import/export module now supports the inclusion of site configurations that belong to exported page trees. Admin users can enable this via the Include site configurations for exported root pages checkbox in the Advanced options tab of the export module. This option is only available to admin users.

When enabled and a page tree belonging to a root page with a site configuration is exported, that configuration is embedded in the export file. On import, the site configuration is restored with the remapped root page UID.

During import, embedded site configurations are processed after all records have been written:

  • The rootPageId is remapped to the newly imported page UID.
  • If a site configuration already exists for the imported root page, the embedded configuration is skipped.
  • If the site identifier already exists but points to a different root page, a numeric suffix is appended (e.g. my-site-1) to avoid collisions.

The import preview screen shows embedded site configurations with their identifier, base URL, and the title of the associated root page.

The CLI export command supports the same feature using the --include-site-configurations option.

Impact 

Site configurations can now be preserved across export and import cycles. This simplifies the distribution of complete site packages and the migration of page trees between TYPO3 instances.

Feature: #109365 - Introduce module access gates for backend modules 

See forge#109365

Description 

The previously hard-coded module access checks (user, admin, systemMaintainer) in the backend module registration have been replaced with an extensible gate system. Each access type is now handled by a dedicated gate class which implements ModuleAccessGateInterface .

TYPO3 ships three built-in gates that preserve existing behavior:

  • UserGate - grants access to admin users and users/groups with explicit module permissions ( be_users.userMods / be_groups.groupMods )
  • AdminGate - grants access only to admin users
  • SystemMaintainerGate - grants access only to system maintainers

Extension authors can register custom gates using the #[AsModuleAccessGate] PHP attribute. A gate receives the module and the current backend user and returns one of three results:

  • ModuleAccessResult::Granted - access is explicitly allowed
  • ModuleAccessResult::Denied - access is explicitly denied
  • ModuleAccessResult::Abstain - the gate cannot decide (not responsible for this access type)

Example: Custom module access gate 

EXT:my_extension/Classes/Module/AccessGate/EditorGate.php
namespace MyVendor\MyExtension\Module\AccessGate;

use TYPO3\CMS\Backend\Module\ModuleAccessGateInterface;
use TYPO3\CMS\Backend\Module\ModuleAccessResult;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Core\Attribute\AsModuleAccessGate;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;

#[AsModuleAccessGate(identifier: 'myEditor')]
final readonly class EditorGate implements ModuleAccessGateInterface
{
    public function decide(
        ModuleInterface $module,
        BackendUserAuthentication $user,
    ): ModuleAccessResult {
        if ($module->getAccess() !== 'myEditor') {
            return ModuleAccessResult::Abstain;
        }
        // Custom logic: Check for a specific user group.
        return in_array(3, $user->userGroupsUID, true)
            ? ModuleAccessResult::Granted
            : ModuleAccessResult::Denied;
    }
}
Copied!

The custom gate can then be referenced in the module registration:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'my_module' => [
        'access' => 'myEditor',
        'labels' => 'my_extension.module',
        // ...
    ],
];
Copied!

Ordering gates 

Gates support before and after parameters to set their evaluation order when multiple gates are registered:

#[AsModuleAccessGate(identifier: 'myEditor', after: ['user'])]
final readonly class EditorGate implements ModuleAccessGateInterface
{
    // ...
}
Copied!

Impact 

Extension authors can now define custom module access strategies beyond the built-in user, admin, and systemMaintainer levels by implementing ModuleAccessGateInterface and registering it with the #[AsModuleAccessGate] attribute.

Existing module registrations using the built-in access values continue to work without changes.

Feature: #109366 - Hide form fields with no selectable items 

See forge#109366

Description 

Relational fields in the FormEngine, such as type=select with foreign_table, type=category, and type=select, with misconfigured empty items are now automatically handled when no selectable items are available. Also, type=language fields are hidden if only a single language is configured, since a dropdown with one choice serves no purpose.

For regular users, the field is removed entirely. When backend debug mode is enabled ( $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] = true ), the field is kept as read-only with an info badge so admins can identify configuration issues such as missing records or restrictive TSconfig.

Common scenarios where this applies:

  • A categories field is displayed but no sys_category records exist.
  • A select field references fe_groups , but no frontend user groups have been created.
  • All available items have been removed via TSconfig removeItems or keepItems configuration.
  • The backend user has no permissions for any of the available items.
  • A site has only one configured language.
  • A type=select field is misconfigured and has no items.

Affected field types 

  • type=select (all render types including selectTree): hidden when no items are available. For fields with foreign_table, static items like "Hide at login" are not counted - they only make sense when actual foreign records exist.
  • type=category: hidden when no items are available
  • type=language: hidden when only one language is available (the special -1 "All languages" item is not counted as a meaningful choice)

Existing values 

Fields not rendered in the form are simply not submitted on save. The DataHandler preserves the existing database value for non-submitted fields, so no data is lost when a field is hidden by this feature.

Opt-out 

The behavior can be disabled per field using the TCA configuration option showIfEmpty:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['columns']['categories']['config']['showIfEmpty'] = true;
Copied!

Impact 

Fields without selectable items are now hidden by default. In backend debug mode, the fields are shown as read-only with an info badge. Extensions that rely on empty fields always being shown should set showIfEmpty to true in their TCA configuration.

Feature: #109409 - Allow configuration of resources 

See forge#109409

Description 

Composer-managed TYPO3 

Until now, extensions could only place public resources in their Resources/Public folder. The folder name used to publish these extension resources was a non-configurable MD5 hash.

This feature introduces the possibility to configure resources explicitly. This includes additional public folders or files that are published in TYPO3's public/_assets folder, as well as non-public resource paths.

TYPO3 classic mode 

In TYPO3 classic mode, there is no visible change for extensions or the typo3/app package, since files in extensions are already located within the document root. Restricting resources to the default locations in classic mode therefore mainly follows coding guidelines and keeps compatibility with Composer mode.

Configuring extensions 

Extensions can add Configuration/Resources.php to configure resources. This configuration is then added to the following default configuration:

EXT:core/Configuration/DefaultPackageResources.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\Resource\Definition\ResourceDefinition;

return static function (Package $package) {
    $resourceDefinitions = [
        new ResourceDefinition('Resources/Private'),
    ];
    if (is_dir($package->getPackagePath() . 'Resources/Public')) {
        $resourceDefinitions[] = new PublicResourceDefinition(
            'Resources/Public',
        );
    }
    return $resourceDefinitions;
};
Copied!

This means that when using the system resources API (Feature: #107537 - System resource API for system file access and public URI generation), resource identifiers are only allowed to reference files or folders in Resources/Private and, if it exists, Resources/Public. It also means that, by default, Resources/Public in extensions is published in the same way as before this change.

Example: Publish an additional public folder 

EXT:my_extension/Configuration/Resources.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;

return static function (Package $package) {
    return [
        new PublicResourceDefinition('Build/Public'),
    ];
};
Copied!

This also publishes the Build/Public folder. The published folder name is a hash unique to this folder.

Example: Publish a single file only 

Instead of publishing a whole folder, a single file can be published:

EXT:my_extension/Configuration/Resources.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicFileDefinition;

return static function (Package $package) {
    return [
        new PublicFileDefinition(relativePath: 'Build/styles.css'),
        new PublicFileDefinition(
            relativePath: 'Build/components.css',
            publicPrefix: $package->getPackageKey() . '/custom/folder/my-components.css',
        ),
    ];
};
Copied!

This publishes the extension file Build/styles.css to a folder with a unique hash, which then contains the file styles.css. Additionally, Build/components.css is published to _assets/my_extension/custom/folder/my-components.css.

Example: Use a fixed prefix in _assets 

By default, TYPO3 generates the public prefix automatically. A fixed prefix can be configured explicitly:

EXT:my_extension/Configuration/Resources.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\Resource\Definition\ResourceDefinition;

return static function (Package $package) {
    return [
        new PublicResourceDefinition(
            relativePath: 'Build/Public',
            publicPrefix: 'my-vendor/my-extension-build',
        ),
    ];
};
Copied!

This publishes resources from Build/Public to a stable location below public/_assets/my-vendor/my-extension-build.

Configuring the typo3/app package 

See the system resources API (Feature: #107537 - System resource API for system file access and public URI generation) for more information about what the typo3/app package represents.

To configure the typo3/app package, a config/system/resources.php file can be added.

If it is missing, the following default configuration is used:

EXT:core/Configuration/DefaultAppResources.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\VirtualAppPackage;

return static function (VirtualAppPackage $package, string $relativePublicPath) {
    return [
        new PublicResourceDefinition(
            relativePath: $relativePublicPath . '_assets',
        ),
        new PublicResourceDefinition(
            relativePath: $relativePublicPath . 'uploads',
        ),
        new PublicResourceDefinition(
            relativePath: $relativePublicPath . 'typo3temp/assets',
        ),
    ];
};
Copied!

Closing notes 

For now, this change mainly affects public files and folders.

Impact 

If resource configuration is not added to an extension, this feature will have no impact on a TYPO3 installation. If such a configuration exists, it extends the default configuration.

Feature: #109412 - Auto-discovery of form YAML configurations 

See forge#109412

Description 

TYPO3 Form Framework can now discover YAML configuration files from every active extension — with no PHP or TypoScript registration required.

The mechanism mirrors how Site Sets work: each extension may provide one or more form sets by placing files in a conventional directory layout. TYPO3 scans all the active extensions and collects every form set it finds, powered by a Symfony service configurator ( FormYamlCollectorConfigurator ) that runs transparently when the form service is first resolved.

Directory layout 

EXT:my_extension/
  Configuration/
    Form/
      MyFormSet/
        config.yaml
Copied!

The subdirectory name (MyFormSet) is arbitrary. An extension may ship multiple sets in separate subdirectories.

The config.yaml 

Contains both the set metadata and the form configuration in a single file. The metadata keys (name, label, priority) are reserved; all other keys are treated as form configuration (prototype definitions, form elements, validators, finishers, rendering options, etc.).

EXT:my_extension/Configuration/Form/MyFormSet/config.yaml
# Unique identifier, vendor/name convention (like composer package names)
name: my-vendor/my-form-set

# Human-readable label for diagnostic output
label: 'My Custom Form Set'

# Load order: lower values are loaded first and act as base configuration.
# Extension sets should use a value > 10 to be merged on top of the
# TYPO3 core base set (typo3/form-base, priority: 10).
# Default: 100
priority: 200

# Form configuration follows directly below the metadata:
persistenceManager:
  allowedExtensionPaths:
    10: 'EXT:my_extension/Resources/Private/Forms/'
Copied!

Priority and merge order 

Sets are sorted by ascending priority (lower = loaded first = acts as base, higher = override). Each set's configuration is merged on top of the previous one using array_replace_recursive(). This is identical to how the former TypoScript mechanism worked.

Migration from TypoScript registration 

Before TYPO3 v14.2, YAML files had to be registered explicitly via TypoScript:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_form.settings.yamlConfigurations {
    1732785702 = EXT:my_extension/Configuration/Form/MySetup.yaml
}
Copied!
# Backend had to be registered separately:
module.tx_form.settings.yamlConfigurations {
    1732785703 = EXT:my_extension/Configuration/Form/MySetup.yaml
}
Copied!

This registration mechanism has been deprecated and will be removed in TYPO3 v15.0. See Deprecation-109412 for details.

After the migration:

  1. Create the directory EXT:my_extension/Configuration/Form/MySet/.
  2. Create config.yaml with name, optionally priority, and the form configuration in the same file.
  3. Remove TypoScript registrations from setup.typoscript.
EXT:my_extension/Configuration/Form/MySet/config.yaml
name: my-vendor/my-form-set
label: 'My Custom Form Set'
priority: 200

# Form configuration (formerly your MySetup.yaml content) goes here:
prototypes:
  standard:
    formElementsDefinition:
      ...
Copied!

Disabling a form set 

Because sets from active extensions are loaded automatically, a mechanism exists to opt out of specific sets without modifying the extension that provides them.

To disable a set, add its declared name (from config.yaml) to $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets'] in ext_localconf.php or config/system/settings.php:

EXT:my_extension/ext_localconf.php
// Disable a third-party form set that conflicts with the site configuration:
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['form']['disabledSets'][]
    = 'some-vendor/conflicting-set';
Copied!

The matching is done against the name field in config.yaml, not against the directory name, so renaming the set directory does not break an existing disable list.

EXT:form base set 

The TYPO3 Form Framework ships its own base set at EXT:form/Configuration/Form/Base/ (typo3/form-base, priority 10). All validators, form elements, finishers, and shared rendering configuration are defined in EXT:form/Configuration/Form/Base/config.yaml.

Site set settings for template paths 

The site set typo3/form provides four settings to override Fluid template paths and translation files.

form.templates.templateRootPath

form.templates.templateRootPath
Type
string
Default
(empty)

Override the default Fluid template path for form element rendering.

form.templates.partialRootPath

form.templates.partialRootPath
Type
string
Default
(empty)

Override the default Fluid partial path for form element rendering.

form.templates.layoutRootPath

form.templates.layoutRootPath
Type
string
Default
(empty)

Override the default Fluid layout path for form element rendering.

form.translation.translationFile

form.translation.translationFile
Type
string
Default
(empty)

Add an additional XLF translation file for form element labels.

Override the template paths in the site configuration:

config/sites/my-site/settings.yaml
form.templates.templateRootPath: EXT:my_sitepackage/Resources/Private/Templates/Form/Frontend/
form.templates.partialRootPath: EXT:my_sitepackage/Resources/Private/Partials/Form/Frontend/
form.templates.layoutRootPath: EXT:my_sitepackage/Resources/Private/Layouts/Form/Frontend/
form.translation.translationFile: EXT:my_sitepackage/Resources/Private/Language/Form/locallang.xlf
Copied!

Impact 

Extensions that have a config.yaml in Configuration/Form/<SetName>/ will have their configuration automatically loaded without any additional registration.

Integrators who include the typo3/form site set can additionally override form template paths, partial paths, layout paths and translation files through site settings — without touching YAML prototype configuration or TypoScript yamlSettingsOverrides .

Feature: #109429 - Introduce PSR-14 ModifyLocalizationHandlerIsAvailableEvent 

See forge#109429

Description 

forge#108049 modernizes the translation workflow in the backend in the Content > Layout and Content > Record module views. Technically, this workflow wizard is backed by localization handlers and finishers.

The PSR-14 event ModifyLocalizationHandlerIsAvailableEvent is now introduced and dispatched in LocalizationHandlerRegistry to allow the availability state of a localization handler to be overridden based on LocalizationInstructions .

The event has the following properties:

  • public readonly string $identifier : String identifier returned by LocalizationHandlerInterface::getIdentifier() from the handler
  • public readonly string $className : The concrete class name in case a handler has been XCLASSed without changing the identifier
  • public readonly LocalizationInstructions $instructions : The localization instructions passed to the handler's isAvailable() method to define the context
  • public bool $isAvailable : The availability state returned by the handler, which can be altered by a PSR-14 event listener

Example 

EXT:my_extension/Classes/EventListener/DisableManualLocalizationHandlerForCustomTableEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Localization\Event\ModifyLocalizationHandlerIsAvailableEvent;
use TYPO3\CMS\Backend\Localization\Handler\ManualLocalizationHandler;
use TYPO3\CMS\Backend\Localization\LocalizationMode;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class DisableManualLocalizationHandlerForCustomTableEventListener
{
    #[AsEventListener(identifier: 'myext/disable-manual-localization-handler-custom-table')]
    public function __invoke(
        ModifyLocalizationHandlerIsAvailableEvent $event,
    ): void {
        if ($event->identifier !== 'manual') {
            // Return early if not ManualLocalizationHandler.
            return;
        }
        if ($event->className !== ManualLocalizationHandler::class) {
            // Return early if the manual identifier is provided but
            // a customized (XCLASSed) class is given. This is just an
            // example for that property.
            return;
        }
        if ($event->instructions->mode !== LocalizationMode::TRANSLATE) {
            // Return early if not handling translation
            // (localization) mode.
            return;
        }

        if ($event->instructions->mainRecordType === 'my_custom_table') {
            // Disallow translation/localization for 'my_custom_table'
            // in general with the default core handler.
            $event->isAvailable = false;
        }
    }
}
Copied!

Impact 

Custom extensions (public and project-specific) are now able to intercept and determine which handlers are available for localization steps in the translation wizard, based on localization context.

Deprecation: #69190 - Deprecate random password generator for frontend and backend users 

See forge#69190

Description 

The passwordRules option of the passwordGenerator field control has been deprecated. Password generation is now configured through password policies registered in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .

Each password policy can define a generator section with a class implementing \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface . The field control references a policy by name via the new passwordPolicy option instead of defining rules inline.

Impact 

Using the passwordRules option in TCA field control configuration will trigger a PHP deprecation warning. Support for passwordRules will be removed in TYPO3 v15.

Affected installations 

Installations that use the passwordGenerator field control with the passwordRules option in custom TCA configurations, for example in password or secret token fields.

Migration 

Replace the passwordRules option with a passwordPolicy reference.

EXT:my_extension/Configuration/TCA/Overrides/be_users.php
'fieldControl' => [
    'passwordGenerator' => [
        'renderType' => 'passwordGenerator',
        'options' => [
-           'passwordRules' => [
-               'length' => 20,
-               'upperCaseCharacters' => true,
-               'lowerCaseCharacters' => true,
-               'digitCharacters' => true,
-               'specialCharacters' => false,
-           ],
+           'passwordPolicy' => 'myCustomPolicy',
        ],
    ],
],
Copied!

The referenced password policy must be registered in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] :

config/system/additional.php OR typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['myCustomPolicy'] = [
    'generator' => [
        'className' => \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGenerator::class,
        'options' => [
            'length' => 20,
            'upperCaseCharacters' => true,
            'lowerCaseCharacters' => true,
            'digitCharacters' => true,
            'specialCharacters' => false,
        ],
    ],
    'validators' => [],
];
Copied!

See Feature: #69190 - Add password generator "wizard" for details on password policies and custom password generators.

Deprecation: #100887 - Deprecation of useNonce argument in f:asset:css and f:asset:script view helpers 

See forge#100887

Description 

The useNonce argument in the f:asset.script and f:asset.css ViewHelpers has been renamed to csp to better reflect its purpose (controlling Content-Security-Policy hash/nonce collection rather than nonce usage specifically).

Similarly, the 'useNonce' asset option key accepted by addJavaScript() and addStyleSheet() in class \TYPO3\CMS\Core\Page\AssetCollector has been replaced by 'csp' .

Impact 

Passing useNonce as a ViewHelper argument or as an AssetCollector option key will trigger a deprecation-level log entry in TYPO3 v14. This usage is scheduled for removal in TYPO3 v15.

Affected installations 

Installations with Fluid templates using <f:asset.script useNonce="1"> or <f:asset.css useNonce="1"> , and extensions calling AssetCollector::addJavaScript() or AssetCollector::addStyleSheet() with ['useNonce' => true] .

Migration 

Replace the useNonce argument with csp in Fluid templates:

<!-- Before -->
<f:asset.script identifier="my-script"
    src="EXT:my_ext/Resources/Public/JavaScript/foo.js"
    useNonce="1" />

<!-- After -->
<f:asset.script identifier="my-script"
    src="EXT:my_ext/Resources/Public/JavaScript/foo.js"
    csp="1" />
Copied!

Replace the 'useNonce' option key with 'csp' in PHP:

// Before
$assetCollector->addJavaScript('my-script', $src, [], ['useNonce' => true]);

// After
$assetCollector->addJavaScript('my-script', $src, [], ['csp' => true]);
Copied!

The PageRenderer methods addJsInlineCode() , addJsFooterInlineCode() , and addCssInlineBlock() retain their $useNonce parameter names for backward compatibility. No migration is required for callers of these methods.

Deprecation: #107068 - Rename fieldExplanationText to description 

See forge#107068

Description 

The configuration option fieldExplanationText has been deprecated in favor of description . The new name better reflects its purpose and is easier to understand.

This affects form element type definitions in prototypes.*.formElementsDefinition.*.formEditor configuration, including editors, validators, and finishers in any extension.

Impact 

Using fieldExplanationText will trigger a PHP deprecation warning. The migration service will automatically convert fieldExplanationText to description when form configuration is loaded, ensuring backward compatibility.

Support for fieldExplanationText will be removed in TYPO3 v15.0.

Affected installations 

Any installations with extensions that provide custom form element type definitions with the configuration option fieldExplanationText in their form prototype YAML files (e.g., Configuration/Form/*.yaml or Configuration/Yaml/FormSetup.yaml).

Migration 

Rename any occurrence of fieldExplanationText to description in your form element type definition YAML files (typically located in Configuration/Yaml/FormElements/*.yaml).

Example migration:

# After
formEditor:
  editors:
    200:
      identifier: placeholder
      label: Placeholder
-     fieldExplanationText: Enter the placeholder text
+     description: Enter the placeholder text
Copied!

Deprecation: #107208 - <f:debug.render> ViewHelper 

See forge#107208

Description 

The <f:debug.render> ViewHelper has been deprecated. It was used internally to render Fluid debug output for the admin panel.

Impact 

Calling the ViewHelper from a template triggers a deprecation warning. The ViewHelper will be removed in TYPO3 v15.

Affected installations 

Projects and extensions that use <f:debug.render> in a template.

Migration 

A custom ViewHelper can be created that mimics the behavior of the Core ViewHelper.

Deprecation: #107802 - Deprecate usage of array in password for authentication in Redis session backend 

See forge#107802

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 via password. With this patch, you can configure the TYPO3 Redis session backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Session\Backend\RedisSessionBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => RedisSessionBackend::class,
    'options' => [
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ]
];
Copied!

Impact 

The "password" configuration option of the Redis session backend is now typed as array|string . Setting this configuration option with an array is deprecated and will be removed in 15.0.

Affected installations 

All installations using a Redis session backend and using the password configuration option to pass an array with a username and password to it.

Migration 

Use the configuration options username and password.

Before:

config/system/additional.php
use TYPO3\CMS\Core\Session\Backend\RedisSessionBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => RedisSessionBackend::class,
    'options' => [
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' =>[
            'user' => 'redis',
            'pass' => 'redis'
        ]
    ]
];
Copied!

After:

config/system/additional.php
use TYPO3\CMS\Core\Session\Backend\RedisSessionBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => RedisSessionBackend::class,
    'options' => [
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ]
];
Copied!

Deprecation: #108345 - Deprecation of ext_emconf.php 

See forge#108345

Description 

TYPO3 extensions that still ship an ext_emconf.php file and do not declare future compatibility to omit this file will now trigger a deprecation message during cache warm-up.

With TYPO3 v15 the ext_emconf.php file is no longer evaluated. For TYPO3 classic (non-Composer) mode to keep working, TYPO3 then needs to know the extension version and which require and suggest entries are not TYPO3 extensions. The extension version and the providesPackages definition therefore become mandatory fields in composer.json, regardless of whether an ext_emconf.php file is still shipped.

In TYPO3 v14 these fields are not yet mandatory, because the deprecated ext_emconf.php can still provide this information. A deprecation message is triggered only when the version or providesPackages field is missing, so that an extension can stay compatible with both TYPO3 v14 and v15 at the same time.

To avoid this deprecation message, the extension must provide the required package metadata in composer.json.

At minimum, this includes the extension version and the providesPackages definition:

composer.json for an extension providing Composer packages
{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "typo3/cms-core": "^14.2",
        "vendor/other-example": "*",
        "symfony/dotenv": "^8.0"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.0.0",
            "Package": {
                "providesPackages": {
                    "symfony/dotenv": "Resources/Private/Php/ComposerVendor"
                }
            }
        }
    }
}
Copied!
composer.json for an extension not providing Composer packages
{
    "name": "vendor/example2",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "license": "GPL-2.0-or-later",
    "require": {
        "typo3/cms-core": "^14.2"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example2_extension",
            "version": "1.0.0",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

For compatibility with TYPO3 classic mode, third-party extensions must set the exact extension version in extra.typo3/cms.version or in the top level version field of composer.json. This version must match the version previously defined in ext_emconf.php and the released Git tag.

Fixture extensions used in tests can set any version number, for example 1.0.0, but a version number must still be provided to avoid deprecation messages.

During testing, the version number is not evaluated.

TYPO3 Core extensions may omit the version number in composer.json because their version number is derived from Typo3Version .

State migration 

The former state field from ext_emconf.php is deprecated as a source of extension metadata and should be set in composer.json using dedicated metadata instead.

Supported stability values should be expressed via the version string:

composer.json using version stability suffixes
{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.2.3-beta2",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

Supported Composer stability values are:

  • dev
  • alpha
  • beta
  • RC
  • stable

State values that are not in the list of supported Composer stability values can be expressed as build metadata:

composer.json using build metadata for custom state labels
{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.0.0+obsolete",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

In this example, obsolete is preserved as build metadata and can still be displayed in the TYPO3 Extension Manager.

The former state = excludeFromUpdates value should now be expressed via a dedicated boolean flag:

composer.json marking an extension as excluded from updates
{
    "name": "vendor/example",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "version": "1.0.0",
            "exclude-from-updates": true,
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

PHP constraints 

If an extension declares a PHP version dependency, it should be in the require section of composer.json:

composer.json defining a PHP version constraint
{
    "name": "vendor/example",
    "version": "1.0.0",
    "type": "typo3-cms-extension",
    "description": "Example extension",
    "require": {
        "typo3/cms-core": "^14.2",
        "php": "^8.2"
    },
    "extra": {
        "typo3/cms": {
            "extension-key": "example_extension",
            "Package": {
                "providesPackages": {}
            }
        }
    }
}
Copied!

The PHP dependency remains relevant for metadata and compatibility checks in TYPO3 classic mode, but it is not used for extension dependency ordering.

If an extension provides regular Composer packages itself in TYPO3 classic mode, these packages must be declared in extra.typo3/cms.Package.providesPackages.

Packages that are already shipped by TYPO3 or already provided by another loaded extension do not need to be repeated there.

Entries in providesPackages may also associate a provided package with a relative path to a Composer vendor directory inside the extension. If that directory contains a Composer-generated autoload.php, TYPO3 includes it early during bootstrap.

If an extension does not provide any regular Composer packages itself, providesPackages must still be present and set to an empty object to avoid deprecation messages and to declare future compatibility with TYPO3 classic mode.

If strict composer.json validation is required and the extension is published to Packagist where setting the top level version field is not recommended, it is recommended to set the version via extra.typo3/cms.version.

If the version field is set anyway, it is recommended to omit extra.typo3/cms.version to avoid redundant data points.

Impact 

There is no impact on Composer-based TYPO3 installations.

TYPO3 classic installations will trigger a deprecation message for extensions that ship a ext_emconf.php and have not defined the required metadata in composer.json.

Affected installations 

TYPO3 classic installations are affected if they use extensions that:

  • still ship ext_emconf.php
  • do not define a "version" field or extra.typo3/cms.version
  • or do not define extra.typo3/cms.Package.providesPackages at all, even as an empty object

Migration 

Extension authors should move extension metadata from ext_emconf.php to composer.json.

This includes:

  • the extension version via "version" or extra.typo3/cms.version
  • providesPackages via extra.typo3/cms.Package.providesPackages, using it for packages provided by the extension itself; packages already shipped by TYPO3 or already provided by another extension do not need to be repeated
  • optional autoload paths for self-provided Composer packages via extra.typo3/cms.Package.providesPackages, pointing to a Composer vendor directory whose autoload.php can be included early
  • supported stability via version suffixes such as -dev, -alpha1, -beta2, or -RC3
  • custom former state labels via build metadata such as +obsolete
  • update exclusion via extra.typo3/cms.exclude-from-updates
  • PHP constraints via the require.php entry

For the time being, ext_emconf.php may still need to be kept for third-party tooling such as TYPO3 TER or Tailor. However, once the required metadata is correctly defined in composer.json, TYPO3 will no longer evaluate ext_emconf.php.

Deprecation: #108557 - TCA option allowedRecordTypes for Page Types 

See forge#108557

Description 

The following methods of \TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry have been marked as deprecated:

  • PageDoktypeRegistry->add()
  • PageDoktypeRegistry->addAllowedRecordTypes()
  • PageDoktypeRegistry->doesDoktypeOnlyAllowSpecifiedRecordTypes()

Impact 

Calling any of the above methods will trigger a deprecation-level log entry and result in a fatal PHP error in TYPO3 v15.0.

Affected installations 

All installations using the PageDoktypeRegistry to configure page types using the add() method. Also, in some rare cases, using the methods addAllowedRecordTypes() or doesDoktypeOnlyAllowSpecifiedRecordTypes() .

Migration 

A new TCA option is introduced to configure allowed record types for pages:

Before:

EXT:my_extension/ext_tables.php
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dokTypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
$dokTypeRegistry->add(
    116,
    [
        'allowedTables' => '*',
    ],
);
Copied!

After:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = ['*'];
Copied!

The array can contain a list of table names or a single entry with an asterisk * to allow all types. If no second argument was provided to the add() method, then the specific configuration can be omitted, as it will fall back to the default allowed records.

Also, note that Page Types are registered by TCA types. The former usage of PageDoktypeRegistry was only useful to define allowed record types different to the default.

The option allowedRecordType is only evaluated in the "pages" table.

Deprecation: #108568 - BackendUserAuthentication::recordEditAccessInternals() and $errorMsg 

See forge#108568

Description 

The method BackendUserAuthentication::recordEditAccessInternals() and the property BackendUserAuthentication::$errorMsg of class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication have been deprecated.

They represented an anti-pattern in which the method returned a boolean value but communicated error details through a class property, making the API difficult to use and test.

A new method checkRecordEditAccess() has been introduced. It returns an \TYPO3\CMS\Core\Authentication\AccessCheckResult value object containing both the access decision and any error messages.

Impact 

Calling the deprecated method recordEditAccessInternals() or accessing the deprecated property $errorMsg will trigger 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 recordEditAccessInternals() or access the $errorMsg property.

Migration 

Replace calls to recordEditAccessInternals() with checkRecordEditAccess() . The new method returns a \TYPO3\CMS\Core\Authentication\AccessCheckResult object with two public properties:

  • isAllowed - Boolean indicating whether access is granted
  • errorMessage - String containing the error message. It is empty if access is granted

Before 

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;

$backendUser = $this->getBackendUser();
if ($backendUser->recordEditAccessInternals($table, $record)) {
    // Access granted
} else {
    // Access denied, error message is in $backendUser->errorMsg
    $errorMessage = $backendUser->errorMsg;
}
Copied!

After 

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;

$backendUser = $this->getBackendUser();
$accessResult = $backendUser->checkRecordEditAccess($table, $record);
if ($accessResult->isAllowed) {
    // Access granted
} else {
    // Access denied
    $errorMessage = $accessResult->errorMessage;
}
Copied!

Deprecation: #108653 - Form file-based storage deprecated 

See forge#108653

Description 

File-based form storage (YAML files stored via file mounts) in EXT:form has been deprecated in favor of database storage.

Since TYPO3 v14.2, the EXT:form module stores form definitions as records in the form_definition database table. This approach provides simpler setup, integrates better with the TYPO3 permission system, and eliminates the need for file mounts and file system configuration.

The following components are deprecated and will be removed in TYPO3 v15.0:

  • \TYPO3\CMS\Form\Storage\FileMountStorageAdapter – the storage adapter for FAL file mount-based form persistence
  • The YAML configuration option persistenceManager.allowedFileMounts – configuring allowed file mounts for form storage

See Feature: #108653 - Database storage for form extension for the new database storage approach.

An upgrade wizard as well as a CLI command are available to migrate existing file-based form definitions to the database: System > Upgrade > Upgrade Wizard > Migrate file-based forms to database storage.

Impact 

File-based form storage will continue to work without any functional changes during the deprecation period. However, it will be removed in TYPO3 v15.0.

An upgrade wizard is available to check whether file-based forms exist and to migrate them to database storage. Run the wizard regularly to verify your migration status.

Affected installations 

All installations that:

  • Store form definitions as YAML files in file mounts (for example, 1:/form_definitions/)
  • Use the persistenceManager.allowedFileMounts configuration option in their form setup YAML with one or more mount points configured

Migration 

  1. Run the upgrade wizard Migrate file-based forms to database storage in the System > Upgrade module. This wizard:

    • Copies all file-based form definitions into the form_definition database table
    • Updates all tt_content FlexForm references (persistenceIdentifier) to point to the new database records
    • Deletes the original YAML files after successful migration

    If the form_definition table does not exist yet, run System > Maintenance > Analyze Database first.

  2. After verifying that all forms work correctly from the database, remove the allowedFileMounts configuration from your YAML setup:

    Before (deprecated):

    EXT:my_extension/Configuration/Yaml/FormSetup.yaml
    persistenceManager:
      allowedFileMounts:
        10: '1:/form_definitions/'
    Copied!

    After:

    To explicitly disable file mount storage, set allowedFileMounts to null ( ~ ):

    EXT:my_extension/Configuration/Yaml/FormSetup.yaml
    persistenceManager:
      allowedFileMounts: ~
    Copied!
  3. Optionally, if the upgrade wizard did not delete the YAML files (e.g., due to file permission issues), delete them manually from the file system after confirming that the migration was successful.

Alternatively, the CLI command form:definition:transfer can be used to transfer forms between storage types:

# Transfer all file mount forms to database
bin/typo3 form:definition:transfer --source=filemount --target=database

# Move (transfer + delete source) in one step
bin/typo3 form:definition:transfer --source=filemount --target=database --move

# Preview without changes
bin/typo3 form:definition:transfer --source=filemount --target=database --dry-run
Copied!

Deprecation: #108843 - ExtensionManagementUtility::addFieldsToUserSettings 

See forge#108843 See forge#108832

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings() has been deprecated in favor of the new addUserSetting() method.

The legacy method required two separate steps to add a field to user settings: first, adding the field configuration to the columns array and second, calling addFieldsToUserSettings() to add it to the showitem list. The new method combines both steps into a single call and uses TCA as the storage location.

Impact 

Calling the deprecated method will trigger a deprecation-level log entry. The method will be removed in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that use ExtensionManagementUtility::addFieldsToUserSettings() or directly modify $GLOBALS['TYPO3_USER_SETTINGS'] to add custom fields to the backend user profile settings.

Migration 

Replace the two-step approach with the new addUserSetting() method. Note that the new method uses TCA-style configuration and should be called from Configuration/TCA/Overrides/be_users.php instead of ext_tables.php.

Before 

// In ext_tables.php
$GLOBALS['TYPO3_USER_SETTINGS']['columns']['myCustomSetting'] = [
    'type' => 'check',
    'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings(
    'myCustomSetting',
    'after:emailMeAtLogin'
);
Copied!

After 

// In Configuration/TCA/Overrides/be_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserSetting(
    'myCustomSetting',
    [
        'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
        ],
    ],
    'after:emailMeAtLogin'
);
Copied!

Field type mapping 

When migrating, use the following type mappings:

Legacy type TCA config
text ['type' => 'input']
email ['type' => 'email']
number ['type' => 'number']
password ['type' => 'password']
check ['type' => 'check', 'renderType' => 'checkboxToggle']
select ['type' => 'select', 'renderType' => 'selectSingle']
language ['type' => 'language']

Deprecation: #108963 - Deprecate PageRenderer->addInlineLanguageDomain() 

See forge#108963

Description 

\TYPO3\CMS\Core\Page\PageRenderer->addInlineLanguageDomain() has been deprecated in favor of importing JavaScript modules, as introduced in Feature: #108941 - Provide language labels as virtual JavaScript modules.

Impact 

Extension developers can now use labels in JavaScript components without requiring labels to be preloaded globally or per module. This reduces the risk of missing labels and simplifies developer workflows.

Affected installations 

The deprecated method was introduced in TYPO3 v14.1. This means that only installations that use addInlineLanguageDomain() in TYPO3 v14.1 or later are affected.

Migration 

The call to PageRenderer::addInlineLanguageDomain() can be removed. In the JavaScript code, add a module import that imports from the '~labels/' prefix.

Before:

$pageRenderer->addInlineLanguageDomain('core.bookmarks');
Copied!
import { html } from 'lit';
import { lll } from '@typo3/core/lit-helper.js';

html`<p>{lll('core.bookmarks:groupType.global')}</p>`
Copied!

After:

import { html } from 'lit';
// Import labels from language domain "core.bookmarks"
import labels from '~labels/core.bookmarks';

// Use label
html`<p>{labels.get('groupType.global')}</p>`
Copied!

Deprecation: #109027 - Move language:update command and events to EXT:core 

See forge#109027

Description 

The language:update CLI command and related \TYPO3\CMS\Install\Service\LanguagePackService have been moved from EXT:install to EXT:core, allowing installations to update language packs without EXT:install having to be installed.

Since TYPO3 v13 it has been possible to run TYPO3 without EXT:install in Composer-based installations. However, the language:update command still required EXT:install, which was impractical for deployments that needed to update language packs.

The following classes have been moved, and their old class names deprecated:

  • \TYPO3\CMS\Install\Command\LanguagePackCommand is now \TYPO3\CMS\Core\Command\UpdateLanguagePackCommand
  • \TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent is now \TYPO3\CMS\Core\Localization\Event\ModifyLanguagePackRemoteBaseUrlEvent
  • \TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent is now \TYPO3\CMS\Core\Localization\Event\ModifyLanguagePacksEvent

The old class names are registered as aliases via ClassAliasMap and continue to work in TYPO3 v14. Event listeners registered for the deprecated event class names are still called when the new event is dispatched, with a deprecation notice triggered at runtime.

Impact 

Using the old class names will trigger a deprecation notice. The extension scanner will report usage of the deprecated class names.

The old class names will be removed in TYPO3 v15.

Affected installations 

Extensions that use one or more of the deprecated class names listed above.

Migration 

Replace the old class names with the new ones in use statements:

EXT:my_extension/Classes/EventListener/MyEventListener.php
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\EventListener;

 use TYPO3\CMS\Core\Attribute\AsEventListener;
-use TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent;
+use TYPO3\CMS\Core\Localization\Event\ModifyLanguagePacksEvent;

 final class MyEventListener
 {
     #[AsEventListener(
         identifier: 'my-extension/modify-language-packs',
     )]
     public function __invoke(
         ModifyLanguagePacksEvent $event,
     ): void {
         // ...
     }
 }
Copied!
EXT:my_extension/Classes/EventListener/MyOtherEventListener.php
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\EventListener;

 use TYPO3\CMS\Core\Attribute\AsEventListener;
-use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;
+use TYPO3\CMS\Core\Localization\Event\ModifyLanguagePackRemoteBaseUrlEvent;

 final class MyOtherEventListener
 {
     #[AsEventListener(
         identifier: 'my-extension/modify-language-pack-remote-base-url',
     )]
     public function __invoke(
         ModifyLanguagePackRemoteBaseUrlEvent $event,
     ): void {
         // ...
     }
 }
Copied!

Deprecation: #109029 - FormEngine doSave hidden field 

See forge#109029

Description 

The <input type="hidden" name="doSave"> field in FormEngine was a legacy mechanism where JavaScript set the field value to 1 to signal to PHP that the submitted form data should be processed as a save operation.

This indirection is no longer needed. TYPO3 now uses native submit button values such as _savedok directly, which are sufficient to determine whether a save operation should be performed. The field is no longer evaluated internally.

For backward compatibility, the doSave field is still appended to the form on programmatic saves in TYPO3 v14, but this behavior is deprecated and will be removed in TYPO3 v15.

Impact 

Third-party code reading $request->getParsedBody()['doSave'] to detect whether a save operation was triggered will stop working in TYPO3 v15.

Affected installations 

Installations with custom backend modules or extensions that inspect the doSave POST field to determine whether incoming form data should be persisted.

Migration 

Replace any checking of the doSave POST field with a check of all native submit action fields that FormEngine sends as part of normal form submission.

Before:

$parsedBody = $request->getParsedBody();
$doSave = (bool)($parsedBody['doSave'] ?? false);
if ($doSave) {
    // process data
}
Copied!

After:

$parsedBody = $request->getParsedBody();
$isSaveAction = !empty($parsedBody['_savedok'])
    || !empty($parsedBody['_saveandclosedok'])
    || !empty($parsedBody['_savedokview'])
    || !empty($parsedBody['_savedoknew']);
if ($isSaveAction) {
    // process data
}
Copied!

Deprecation: #109102 - FormEngine "additionalHiddenFields" key 

See forge#109102

Description 

The additionalHiddenFields result array key in FormEngine was a legacy mechanism that stored hidden <input> HTML strings separately from the main html key. This indirection is no longer needed. Elements can simply add their hidden fields to the html key.

The following have been deprecated:

  • The additionalHiddenFields key in FormEngine result arrays
  • FormResult::$hiddenFieldsHtml
  • FormResultCollection::getHiddenFieldsHtml()

Impact 

Third-party FormEngine elements that add entries to $resultArray['additionalHiddenFields'] will trigger a PHP E_USER_DEPRECATED level error when their result is merged via AbstractNode::mergeChildReturnIntoExistingResult() .

Affected installations 

Installations with custom FormEngine elements or containers that populate the additionalHiddenFields result array key.

Migration 

Move hidden field HTML from additionalHiddenFields into the html key.

Before:

$resultArray = $this->initializeResultArray();
$resultArray['additionalHiddenFields'][] =
    '<input type="hidden" name="myField" value="myValue" />';
Copied!

After:

$resultArray = $this->initializeResultArray();
$resultArray['html'] .=
    '<input type="hidden" name="myField" value="myValue" />';
Copied!

Deprecation: #109152 - Form DatePicker element 

See forge#109152

Description 

The DatePicker form element type and its associated DatePickerViewHelper and TimePickerViewHelper have been deprecated as part of removing jQuery dependency from typo3/cms-form . The Date form element type serves as a replacement and uses native HTML5 <input type="date"> without needing a JavaScript library.

The following components are deprecated:

  • \TYPO3\CMS\Form\Domain\Model\FormElements\DatePicker
  • \TYPO3\CMS\Form\ViewHelpers\Form\DatePickerViewHelper
  • \TYPO3\CMS\Form\ViewHelpers\Form\TimePickerViewHelper
  • The EXT:form/Resources/Public/JavaScript/frontend/date-picker.js jQuery initialization script

Impact 

Using the DatePicker form element type in a form definition will trigger a PHP E_USER_DEPRECATED level error at runtime. The element, its ViewHelpers, and the JavaScript file will be removed in TYPO3 v15.

Affected installations 

All installations that use the DatePicker form element type in form definitions created with the TYPO3 Form Framework.

Migration 

Replace DatePicker with the Date form element type in your form definitions.

Before:

type: DatePicker
identifier: date-1
label: 'Pick a date'
properties:
  dateFormat: Y-m-d
  enableDatePicker: true
Copied!

After:

type: Date
identifier: date-1
label: 'Pick a date'
Copied!

The Date element uses a native HTML5 date input, which does not require jQuery or additional JavaScript. The dateFormat and enableDatePicker properties are no longer needed because the browser handles date formatting and the picker natively.

Alternatively, if the native HTML5 date input does not meet your requirements, you can create a custom form element with a date picker JavaScript library of your choice.

Deprecation: #109171 - Bootstrap tab events 

See forge#109171

Description 

Bootstrap's tab JavaScript has been replaced with a custom implementation tailored to TYPO3. The Bootstrap tab events show.bs.tab and shown.bs.tab are now deprecated and will be removed in TYPO3 v15.

The following new custom events are available as replacements:

  • typo3:tab:show — dispatched before a tab switch, cancelable via event.preventDefault()
  • typo3:tab:shown — dispatched after a tab switch

Both events bubble from the tab button and carry a detail.relatedTarget property that points to the previously active tab button or null .

Impact 

Listening for show.bs.tab or shown.bs.tab events will continue to work in TYPO3 v14 but will stop working in TYPO3 v15, when the backward- compatibility events will be removed.

Affected installations 

All extensions that listen to show.bs.tab or shown.bs.tab events on tab buttons are affected.

Migration 

Replace Bootstrap tab event listeners with the new TYPO3 tab events.

Before:

document.addEventListener('show.bs.tab', (e) => {
  console.log(
    'Tab is about to show',
    e.target,
    e.detail.relatedTarget
  );
});

document.addEventListener('shown.bs.tab', (e) => {
  console.log(
    'Tab was shown',
    e.target,
    e.detail.relatedTarget
  );
});
Copied!

After:

document.addEventListener('typo3:tab:show', (e) => {
  console.log('Tab is about to show', e.target, e.detail.relatedTarget);
});

document.addEventListener('typo3:tab:shown', (e) => {
  console.log('Tab was shown', e.target, e.detail.relatedTarget);
});
Copied!

Deprecation: #109192 - FormEngine OuterWrapContainer 

See forge#109192

Description 

The \TYPO3\CMS\Backend\Form\Container\OuterWrapContainer FormEngine container has been deprecated in favor of the new \TYPO3\CMS\Backend\Form\Container\FormWrapContainer . The old container rendered record headers, type icons, and record identity information inside FormEngine, which forced controllers to hide redundant elements via CSS hacks.

The new FormWrapContainer only handles form wrapping (description, read-only notice, field information, field wizards, and child HTML). Rendering record headers and identity information is now the responsibility of the controllers themselves.

Impact 

Using the outerWrapContainer render type will trigger a PHP E_USER_DEPRECATED level error. The container will still work as before during the deprecation period.

Affected installations 

Installations with custom controllers or FormEngine integrations that set $formData['renderType'] = 'outerWrapContainer' .

Migration 

Replace the render type outerWrapContainer with formWrapContainer.

Before:

$formData['renderType'] = 'outerWrapContainer';
$formResult = $this->nodeFactory->create($formData)->render();
Copied!

After:

$formData['renderType'] = 'formWrapContainer';
$formResult = $this->nodeFactory->create($formData)->render();
Copied!

Note that FormWrapContainer no longer renders the record heading ( <h1> ) or the record identity footer (icon, table title, uid). If your controller relied on these being rendered by OuterWrapContainer , you need to render them in your controller code.

Deprecation: #109196 - Deprecate doktypesToShowInNewPageDragArea user TSconfig 

See forge#109196

Description 

The user TSconfig option options.pageTree.doktypesToShowInNewPageDragArea has been deprecated and will be removed in TYPO3 v15.0.

The page tree toolbar submenu now automatically determines available doktypes based on the user's group permissions. Manual TSconfig configuration is no longer needed.

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 that set options.pageTree.doktypesToShowInNewPageDragArea in their user TSconfig.

Migration 

Remove the options.pageTree.doktypesToShowInNewPageDragArea option from your user TSconfig. The page tree toolbar will then display all doktypes that the current backend user is allowed to create based on their group permissions.

Deprecation: #109230 - FormResultCompiler 

See forge#109230

Description 

The class \TYPO3\CMS\Backend\Form\FormResultCompiler has been deprecated. The internal implementation of FormEngine has been adjusted to better separate concerns, especially regarding rendering and asset handling. This change also removed all internal usages of \TYPO3\CMS\Backend\Form\FormResultCompiler , as it handled more tasks than its name suggested.

Impact 

Extensions and installations that render FormEngine forms manually rather than through standard controllers, such as EditDocumentController , and that use FormResultCompiler , will be affected when the class is removed in TYPO3 v15.

Affected installations 

Installations and extensions using FormResultCompiler to build FormEngine forms.

Migration 

Replace FormResultCompiler with FormResultFactory and FormResultHandler .

Before:

use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\FormResultCompiler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
$formResultCompiler = GeneralUtility::makeInstance(
    FormResultCompiler::class
);

$formResult = $nodeFactory->create($formData)->render();
$formResultCompiler->mergeResult($formResult);

// Form HTML markup is accessible in the data array
$body = $formResult['html'];
Copied!

After:

use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\FormResultFactory;
use TYPO3\CMS\Backend\Form\FormResultHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
$formResultFactory = GeneralUtility::makeInstance(
    FormResultFactory::class
);
$formResultHandler = GeneralUtility::makeInstance(
    FormResultHandler::class
);

$formResult = $nodeFactory->create($formData)->render();
// Convert the raw result array into a FormResult object
$formResult = $formResultFactory->create($formResult);

// Use FormResultHandler to pass collected assets (JS, CSS, labels) to PageRenderer
$formResultHandler->addAssets($formResult);

// Form HTML markup is accessible in the FormResult DTO
$body = $formResult->html;
Copied!

Deprecation: #109280 - FormEngine TcaDescription fieldInformation 

See forge#109280

Description 

The \TYPO3\CMS\Backend\Form\FieldInformation\TcaDescription field information render type has been deprecated. Field descriptions configured via TCA ['columns']['fieldName']['description'] are now rendered automatically next to the field label by \TYPO3\CMS\Backend\Form\Element\AbstractFormElement::renderDescription() and \TYPO3\CMS\Backend\Form\Container\AbstractContainer::renderDescription() .

Previously, every FormEngine element and container registered tcaDescription as a default field information node, which rendered the description inside the element body. The description is now rendered after the label or legend element, providing more consistent positioning across all field types.

Additionally, the $defaultFieldInformation property has been removed from all Core FormEngine elements and containers. Custom elements that extend Core elements and rely on tcaDescription being present in $defaultFieldInformation are also affected.

Impact 

Using the tcaDescription render type in a custom fieldInformation configuration will trigger a PHP E_USER_DEPRECATED level error. The render type still exists but will return empty output during the deprecation period, since descriptions are now rendered at the label level.

Custom FormEngine nodes that extend core elements and override $defaultFieldInformation to include tcaDescription will still work, but the tcaDescription entry will trigger a deprecation warning.

Affected installations 

  • Installations with extensions that explicitly configure tcaDescription as a field information node in TCA:

    'fieldInformation' => [
        'tcaDescription' => [
            'renderType' => 'tcaDescription',
        ],
    ],
    Copied!
  • Custom FormEngine elements and containers that set tcaDescription in their $defaultFieldInformation property:

    protected $defaultFieldInformation = [
        'tcaDescription' => [
            'renderType' => 'tcaDescription',
        ],
    ];
    Copied!

Extensions that only use the standard TCA description property are not affected — descriptions will continue to be rendered.

Migration 

Remove any explicit tcaDescription field information configuration from TCA and from custom FormEngine node classes. Field descriptions are now rendered automatically next to the label and no longer require a field information node.

TCA configuration

 'columns' => [
     'my_field' => [
         'label' => 'My field',
         'description' => 'Help text for this field',
         'config' => [
             'type' => 'input',
-            'fieldInformation' => [
-                'tcaDescription' => [
-                    'renderType' => 'tcaDescription',
-                ],
-            ],
         ],
     ],
 ],
Copied!

Custom FormEngine nodes with defaultFieldInformation

If your custom element had a tcaDescription in $defaultFieldInformation , remove the property entirely:

class MyCustomElement extends AbstractFormElement
{
-    protected $defaultFieldInformation = [
-        'tcaDescription' => [
-            'renderType' => 'tcaDescription',
-        ],
-    ];
+    // tcaDescription is no longer needed; descriptions are
+    // rendered automatically next to the label.
}
Copied!

If your custom element has other field information entries alongside tcaDescription, remove only the tcaDescription entry:

class MyCustomElement extends AbstractFormElement
{
    protected $defaultFieldInformation = [
-       'tcaDescription' => [
-           'renderType' => 'tcaDescription',
-       ],
        'myCustomInfo' => [
            'renderType' => 'myCustomInfo',
        ],
    ];
}
Copied!

Deprecation: #109286 - Explicit request handling in PageRenderer 

See forge#109286

Description 

Since TYPO3 v14.2 some methods of class \TYPO3\CMS\Core\Page\PageRenderer require an instance of ServerRequestInterface to be passed explicitly :

setLanguage() 

  • Old: PageRenderer->setLanguage(Locale $locale, ?ServerRequestInterface $request = null)
  • TYPO3 v14.2: PageRenderer->setLanguage(Locale $locale, ?ServerRequestInterface $request = null)
  • TYPO3 v15: PageRenderer->setLanguage(Locale $locale, ServerRequestInterface $request)

setDocType() 

  • Old: PageRenderer->setDocType(DocType $docType)
  • TYPO3 v14.2: PageRenderer->setDocType(DocType $docType, ?ServerRequestInterface $request = null)
  • TYPO3 v15: PageRenderer->setDocType(DocType $docType, ServerRequestInterface $request)

render() 

  • Old: PageRenderer->render()
  • TYPO3 v14.2: PageRenderer->render(?ServerRequestInterface $request = null)
  • TYPO3 v15: PageRenderer->render(ServerRequestInterface $request)

renderResponse() 

  • Old: PageRenderer->renderResponse(int $code = 200, string $reasonPhrase = '')
  • TYPO3 v14.2: PageRenderer->render(ServerRequestInterface|int $requestOrCode = 200, int|string $codeOrReasonPhrase = '', string $reasonPhrase = '')
  • TYPO3 v15: PageRenderer->render(ServerRequestInterface $request, int $code = 200, string $reasonPhrase = '')

Impact 

Request dependencies within PageRenderer are no longer implicit via $GLOBALS['TYPO3_REQUEST'] and must now be passed explicitly. Not passing a request to the methods listed above will trigger a deprecation-level log entry in TYPO3 v14 and will result in a fatal PHP error in TYPO3 v15.

Affected installations 

PageRenderer is a low-level Core class. Many extensions use higher-level APIs and are therefore not directly affected by this change.

Migration 

Adapt method calls to pass the ServerRequestInterface object explicitly.

Deprecation: #109295 - DatabaseWriter::setLogTable()/getLogTable() 

See forge#109295

Description 

The methods setLogTable() and getLogTable() in \TYPO3\CMS\Core\Log\Writer\DatabaseWriter have been deprecated.

DatabaseWriter is a dedicated writer for the sys_log table. Its writeLog() method maps LogRecord fields to the sys_log schema ( request_id , time_micro , component , level , message , data , tstamp ). Allowing an arbitrary table to be set via setLogTable() created a false sense of flexibility - any custom table needs to replicate the full sys_log schema to work correctly.

The long-term goal is to make DatabaseWriter final and to remove the $logTable property entirely.

Impact 

Calling setLogTable() or getLogTable() triggers a PHP E_USER_DEPRECATED error. This also includes passing logTable as a configuration option when DatabaseWriter is registered via $GLOBALS['TYPO3_CONF_VARS']['LOG'] , since the AbstractWriter constructor resolves options to set*() calls.

Support will be removed in TYPO3 v15.0.

Affected installations 

Installations that configure DatabaseWriter with a custom logTable option, or that call setLogTable() or getLogTable() on a DatabaseWriter instance.

The extension scanner detects direct calls to ->setLogTable() and ->getLogTable() . The more common case, passing logTable as a configuration option via $GLOBALS['TYPO3_CONF_VARS']['LOG'] , cannot be detected automatically and requires a manual search for DatabaseWriter usage with a logTable key.

Migration 

Replace DatabaseWriter with a dedicated writer that extends \TYPO3\CMS\Core\Log\Writer\AbstractWriter and implements writeLog() with explicit field mapping for the custom table.

Before:

use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\DatabaseWriter;

$GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'][LogLevel::WARNING] =
[
    DatabaseWriter::class => ['logTable' => 'my_custom_log'],
];
Copied!

After:

EXT:my_extension/Classes/Log/Writer/MyCustomTableWriter.php
namespace MyVendor\MyExtension\Log\Writer;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Log\Writer\AbstractWriter;
use TYPO3\CMS\Core\Log\Writer\WriterInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class MyCustomTableWriter extends AbstractWriter
{
    public function writeLog(LogRecord $record): WriterInterface
    {
        GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionForTable('my_custom_log')
            ->insert('my_custom_log', [
                'created' => (int)$record->getCreated(),
                'level' => $record->getLevel(),
                'message' => $record->getMessage(),
            ]);
        return $this;
    }
}
Copied!
use Psr\Log\LogLevel;
use MyVendor\MyExtension\Log\Writer\MyCustomTableWriter;

$GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'][LogLevel::WARNING] = [
    MyCustomTableWriter::class => [],
];
Copied!

Deprecation: #109306 - Deprecate form editor stage template rendering functions 

See forge#109306

Description 

The Form Editor stage component provided a set of JavaScript helper functions for template-based rendering of form elements in the stage area. These functions were designed to be called from subscribers to the view/stage/abstract/render/template/perform PubSub event, which is the extension point for custom form element rendering in the stage.

With the introduction of the <typo3-form-form-element-stage-item> and <typo3-form-page-stage-item> web components (see Feature: #107058 - Simplify registration of a custom form element), the built-in template-based helper functions have been superseded. The view/stage/abstract/render/template/perform event remains available, and extension authors may continue to subscribe to it to implement fully custom stage rendering logic.

The following exported functions from @typo3/form/backend/form-editor/stage-component are deprecated:

  • eachTemplateProperty()
  • renderSimpleTemplate()
  • renderSimpleTemplateWithValidators()
  • renderCheckboxTemplate()
  • renderSelectTemplates()
  • renderFileUploadTemplates()
  • createAbstractViewFormElementToolbar() — only used by the legacy template-based rendering path. Web component-based elements handle their toolbar via the toolbarConfig property of <typo3-form-form-element-stage-item>

In addition, all Fluid partial templates in EXT:form/Resources/Private/Backend/Partials/FormEditor/Stage/ are deprecated, as they were designed for use with the template-based rendering approach described above:

  • SimpleTemplate.fluid.html
  • SelectTemplate.fluid.html
  • FileUploadTemplate.fluid.html
  • ContentElement.fluid.html
  • Fieldset.fluid.html
  • StaticText.fluid.html
  • Page.fluid.html
  • SummaryPage.fluid.html
  • _ElementToolbar.fluid.html
  • _UnknownElement.fluid.html

Impact 

Extensions that call any of the deprecated helper functions will receive IDE deprecation hints and TypeScript compiler warnings. The deprecated Fluid templates will emit an HTML comment in the rendered stage area indicating their deprecation. All deprecated functions and templates will be removed in TYPO3 v15.

Affected installations 

All extensions that:

  • call any of the deprecated JavaScript helper functions (including createAbstractViewFormElementToolbar() ), typically from a subscriber of the view/stage/abstract/render/template/perform event, or
  • reference any of the deprecated Fluid partial templates via formEditorPartials in their prototype configuration.

Migration 

Two migration paths are available:

Option 1: Use the built-in web component (recommended)

Remove the custom JavaScript subscriber and omit the formEditorPartials stage partial configuration from your form element's YAML definition. The Form Editor will then render the element automatically using the built-in <typo3-form-form-element-stage-item> web component.

See Feature: #107058 - Simplify registration of a custom form element for full details.

Option 2: Implement custom rendering logic in the event subscriber

If you need to keep using the view/stage/abstract/render/template/perform event, replace calls to the deprecated helper functions with your own DOM manipulation logic.

Deprecation: #109329 - PageRenderer get() methods 

See forge#109329

Description 

The following methods have been deprecated:

  • TYPO3\CMS\Core\Page\PageRenderer->getTitle()
  • TYPO3\CMS\Core\Page\PageRenderer->getLanguage()
  • TYPO3\CMS\Core\Page\PageRenderer->getDocType()
  • TYPO3\CMS\Core\Page\PageRenderer->getHtmlTag()
  • TYPO3\CMS\Core\Page\PageRenderer->getHeadTag()
  • TYPO3\CMS\Core\Page\PageRenderer->getFavIcon()
  • TYPO3\CMS\Core\Page\PageRenderer->getIconMimeType()
  • TYPO3\CMS\Core\Page\PageRenderer->getTemplateFile()
  • TYPO3\CMS\Core\Page\PageRenderer->getMoveJsFromHeaderToFooter()
  • TYPO3\CMS\Core\Page\PageRenderer->getBodyContent()
  • TYPO3\CMS\Core\Page\PageRenderer->getInlineLanguageLabels()
  • TYPO3\CMS\Core\Page\PageRenderer->getInlineLanguageLabelFiles()
  • TYPO3\CMS\Core\Page\PageRenderer->getMetaTag()
  • TYPO3\CMS\Core\Page\PageRenderer->removeMetaTag()
  • TYPO3\CMS\Frontend\ContentObject\AbstractContentObject->getPageRenderer()

Impact 

Invoking any of the methods listed above will generate a deprecation-level log entry in TYPO3 v14. These methods are scheduled for removal in TYPO3 v15.

From an architectural perspective, the PageRenderer singleton represents a central yet problematic construct, particularly in TYPO3 frontend rendering. With the deprecation of these methods, the PageRenderer class loses its ability to serve as a data source - data can still be added but no longer retrieved.

This change paves the way for refactoring the construct in TYPO3 v15, including the introduction of a compatibility layer to maintain backward compatibility.

Affected installations 

Instances with extensions invoking one of the methods listed above are affected. The extension scanner is configured to find consumers, apart from the generic method names getTitle() , getLanguage() , and getPageRenderer() .

Migration 

In practice, there is often little reason to rely on the methods mentioned above. Most data passed to PageRenderer is handled through mechanisms that can be intercepted and configured, for example title and meta tag handling. As a result, the deprecated get() methods do not have a direct replacement.

A commonly used case is PageRenderer->getDocType() , which determines whether self-closing tags should include a trailing slash (/). This is relevant only in the frontend, as the backend always uses HTML5. The DocType itself is derived from TypoScript configuration, which is available as a request attribute.

Before:

$needsEndingSlash = GeneralUtility::makeInstance(PageRenderer::class)
    ->getDocType()
    ->isXmlCompliant();
Copied!

After:

$needsEndingSlash = DocType::createFromRequest($request)
    ->isXmlCompliant();
Copied!

Deprecation: #109409 - Access to arbitrary resources in extensions 

See forge#109409

Description 

Accessing extension resources outside the configured resource definitions is deprecated.

By default, extension resources are limited to the following paths:

  • Configuration
  • Resources/Private
  • Resources/Public

If a resource identifier references another extension path, that path must be configured explicitly in Configuration/Resources.php.

See Feature: #109409 - Allow configuration of resources for information on how to configure resources for extensions.

Impact 

TYPO3 installations using resource identifiers that reference extension folders outside Configuration, Resources/Private, or Resources/Public will receive a deprecation message when such a resource is resolved.

Every accessed resource must be configured beforehand as described in Feature: #109409 - Allow configuration of resources.

Affected installations 

TYPO3 installations using resource identifiers that reference extension folders outside Configuration, Resources/Private, or Resources/Public.

Migration 

Either configure the referenced paths explicitly in Configuration/Resources.php, as described in Feature: #109409 - Allow configuration of resources, or move the resources to a path that is already configured.

Deprecation: #109409 - Allowed paths configuration is deprecated 

See forge#109409

Description 

Using $GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths'] to configure additional public paths for the typo3/app package has been deprecated.

Configure resources in config/system/resources.php instead. See Feature: #109409 - Allow configuration of resources for details.

Impact 

TYPO3 installations that use $GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths'] will receive a deprecation message whenever resources for the typo3/app package are resolved.

Affected installations 

TYPO3 installations that use $GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths'] .

Migration 

Configure resources in config/system/resources.php instead of using $GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths'] .

See Feature: #109409 - Allow configuration of resources for information on how to configure resources for the typo3/app package.

Deprecation: #109412 - TypoScript-based form YAML registration 

See forge#109412

Description 

The TypoScript-based registration of form YAML configuration files via plugin.tx_form.settings.yamlConfigurations and module.tx_form.settings.yamlConfigurations has been deprecated in favor of the new auto-discovery mechanism introduced in TYPO3 v14.2 (see Feature-109412).

Before TYPO3 v14.2 this was the only way to register EXT:form YAML files. It required separate registration of the frontend and the backend in TypoScript:

EXT:my_extension/Configuration/TypoScript/setup.typoscript — deprecated
plugin.tx_form.settings.yamlConfigurations {
    1732785702 = EXT:my_extension/Configuration/Form/MySetup.yaml
}

# Backend had to be registered separately:
module.tx_form.settings.yamlConfigurations {
    1732785703 = EXT:my_extension/Configuration/Form/MySetup.yaml
}
Copied!

The TypoScript-based paths will still be loaded during the deprecation period but will be removed in TYPO3 v15.0.

Impact 

Extensions that register form YAML files via TypoScript will trigger a PHP E_USER_DEPRECATED error. The registered YAML files are still loaded and will remain functional during the deprecation period.

Affected installations 

All installations where an extension registers form YAML files via:

  • plugin.tx_form.settings.yamlConfigurations
  • module.tx_form.settings.yamlConfigurations

Migration 

Replace TypoScript registration with the auto-discovery directory convention introduced in TYPO3 v14.2 (see Feature-109412).

  1. Create directory EXT:my_extension/Configuration/Form/MySet/.
  2. Add a config.yaml file with a unique name and, optionally, a priority value (default: 100; the core base set is priority 10):

    EXT:my_extension/Configuration/Form/MySet/config.yaml
    name: my-vendor/my-form-set
    label: 'My Custom Form Set'
    priority: 200
    Copied!
  3. Add your existing form configuration to config.yaml below the metadata keys:

    EXT:my_extension/Configuration/Form/MySet/config.yaml
    name: my-vendor/my-form-set
    label: 'My Custom Form Set'
    priority: 200
    
    # Content of your former MySetup.yaml
    persistenceManager:
      allowedExtensionPaths:
        10: 'EXT:my_extension/Resources/Private/Forms/'
    Copied!
  4. Remove TypoScript registrations from setup.typoscript. PHP or TypoScript registration is no longer necessary.

The YAML files are picked up automatically for both frontend and backend without any additional registration.

Important: #70867 - XLIFF whitespace handling now respects xml:space 

See forge#70867

Description 

TYPO3's XLIFF parser now respects the xml:space attribute according to the XML specification (https://www.w3.org/TR/xml/#sec-white-space).

This affects how whitespace (spaces, tabs, newlines) in translation strings is handled.

Without xml:space="preserve" (default behavior):

Multiple consecutive whitespace characters (spaces, tabs, newlines) are collapsed into a single space, and leading/trailing whitespace is trimmed.

Example XLIFF source:

<trans-unit id="my.label">
  <source>This is a
    multi-line
    string.</source>
</trans-unit>
Copied!

Before: The string contained literal newlines and indentation. After: The string becomes "This is a multi-line string."

With xml:space="preserve" :

Whitespace is kept exactly as written in the XLIFF file.

<trans-unit id="my.label" xml:space="preserve">
  <source>This is a
    multi-line
    string.</source>
</trans-unit>
Copied!

The string remains "This is a\n multi-line\n string."

Impact 

Translation strings that previously contained unintended whitespace (from formatting in the XLIFF file) are now correctly displayed without extra spaces or line breaks.

If you need preserved whitespace in a translation string, add the xml:space="preserve" attribute to the <trans-unit> element (XLIFF 1.2) or <segment> element (XLIFF 2.0).

This change affects both XLIFF 1.2 and XLIFF 2.0/2.1 formats.

Important: #93765 - Extbase identity map now language-aware 

See forge#93765

Description 

The Extbase persistence session's identity map now includes language context when domain objects are cached. Previously, the identity map used identifiers based only on a record's UID and localized UID, which could cause incorrect translations to be returned when the same object was accessed by different LanguageAspect configuration inside the same request.

The identity map identifier now includes the language context, specifically the contentId, overlayType, and fallbackChain properties of LanguageAspect .

Impact 

This change ensures that objects loaded with different language configurations are cached separately in the identity map. For example, if an object is first loaded with OVERLAYS_ON and then queried again with OVERLAYS_MIXED, the system will correctly return different cached objects for each context.

The change is transparent for most use cases. However, objects retrieved with different language settings are now distinct instances. Code relying on object identity (for example, using === comparison) between objects loaded with different language settings will need adjustment.

Important: #102906 - Prevent Extbase errorAction from writing session data 

See forge#102906

Description 

Previously, validation errors handled implicitly by the Extbase ActionController::errorAction() persisted the resulting FlashMessage items to the user session. If no session existed, a new session was generated and a session cookie was sent to the client. This behavior could lead to automated crawlers generating a large number of unnecessary sessions.

When errorAction() is invoked (for example, due to validation errors), flash messages are no longer persisted to the session but are instead transferred with the corresponding ForwardResponse .

The implementation introduces two new public methods in \TYPO3\CMS\Extbase\Http\ForwardResponse :

  • withFlashMessages(FlashMessage ...$flashMessages) - Adds flash messages to the forward response
  • getFlashMessages() - Retrieves flash messages from the forward response

Flash messages are transferred through ExtbaseRequestParameters when forwarding requests and are restored from ExtbaseRequestParameters in ActionController::initializeStateFromExtbaseRequestParameters() .

Important: #105441 - TCA select fields with null item values create nullable columns 

See forge#105441

Description 

When a TCA select field is configured as renderType => 'selectSingle' and an item is added with 'value' => null, the database column that is generated is now nullable regardless of whether the other item values are integers or strings.

Previously, the following configuration with integer item values incorrectly generated a VARCHAR(255) column:

'config' => [
    'type' => 'select',
    'renderType' => 'selectSingle',
    'foreign_table' => 'some_table',
    'default' => null,
    'items' => [
        ['label' => 'Please choose', 'value' => null],
    ],
],
Copied!

This now correctly generates INT UNSIGNED DEFAULT NULL .

Similarly, configuration with string item values now also generates a nullable column:

'config' => [
    'type' => 'select',
    'renderType' => 'selectSingle',
    'default' => null,
    'items' => [
        ['label' => 'Default', 'value' => null],
        ['label' => 'Option', 'value' => 'some_value'],
    ],
],
Copied!

This now correctly generates VARCHAR(255) DEFAULT NULL instead of VARCHAR(255) DEFAULT '' NOT NULL .

Important: #108433 - Workspace selector moved to sidebar with color and description 

See forge#108433

Description 

The workspace selector has been moved from the top toolbar to the backend sidebar. The selector now displays the full name of the currently active workspace and provides a dropdown to switch between available workspaces.

A colored top bar indicator is shown whenever a workspace is active, giving editors a clear visual cue about which workspace they are working in.

Color per workspace 

Administrators can now assign a color to each workspace via the Color field in the workspace record. The selected color is used for the top bar indicator and the workspace selector in the sidebar, making it easier to visually distinguish workspaces at a glance.

The following colors are available: red, orange, yellow, lime, green, teal, blue, indigo, purple, and magenta. The default color for new workspaces is orange.

Description as tooltip 

The Description field of a workspace record is now displayed as a tooltip on both the workspace selector dropdown items and the top bar indicator. This allows administrators to provide additional context about the purpose of a workspace, which editors can see by hovering over the workspace name.

Workspace Live indicator option 

The user setting Enable workspace Live indicator is available in User Settings > Personalization. When enabled (the default), the top bar indicator is shown while working in the Live workspace. Users who find the indicator distracting can disable it. The setting takes effect immediately without requiring a page reload. The indicator remains visible when any non-Live workspace is active, regardless of this setting.

Impact 

Editors will see the workspace selector in the sidebar instead of the top toolbar. The workspace indicator bar at the top of the backend now reflects the color that is configured for the active workspace.

Administrators are encouraged to assign meaningful colors and descriptions to their workspaces to improve the editing experience for their teams.

Important: #108557 - Drop PageDoktypeRegistry onlyAllowedTables option 

See forge#108557

Description 

It is possible to limit which tables are allowed for page types (doktype). However, up until now the default behavior when switching types was to ignore violations of these rules. The behavior could be changed for a particular doktype like this:

EXT:my_extension/ext_tables.php
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dokTypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
$dokTypeRegistry->add(
    116,
    [
        'onlyAllowedTables' => true,
    ],
);
Copied!

This made page type 116 strict when switching the page type.

This option is now obsolete, as this functionality is always enabled. Switching page types is no longer possible if it violates the configured allowed tables, which makes the system more consistent.

Some remarks: This option was rarely used and often misunderstood. The option to configure allowed tables was called allowedTables. It was not clear what onlyAllowedTables meant without looking in the documentation.

From a practical point of view, it does not make sense to configure restrictions for page types when they are ignored by default for the action of switching types. Allowing the rules to be violated makes them ineffective in the first place. So either remove those restrictions altogether or make them always apply, which is what happens now.

Important: #108783 - Backend user language default changed to "en" 

See forge#108783

Description 

The backend user language field (be_users.lang) historically used default as the value for English. This has been changed to use the standard ISO 639-1 language code en instead.

The language key default is still accepted for backward compatibility with custom code, but can no longer be selected in the backend user interface.

An upgrade wizard, Migrate backend user language from 'default' to 'en', is available to migrate existing backend user records.

Impact 

  • New backend users have en as their default language instead of default.
  • Existing backend users with lang=default should run the upgrade wizard to migrate to lang=en.
  • In general, code that uses default as a language key (e.g. custom instances of LanguageService ) will continue to work, as default is still mapped to en internally.

Important: #108796 - Internal shortcut classes renamed to bookmark 

See forge#108796

Description 

As part of the centralized bookmark management feature, several internal classes related to backend shortcuts have been renamed to use "bookmark" terminology. These classes were marked as @internal and are not part of the public TYPO3 API. However, some extensions might have used them although they were internal.

The following classes have been renamed or replaced:

\TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository
Removed and replaced by \TYPO3\CMS\Backend\Backend\Bookmark\BookmarkService and \TYPO3\CMS\Backend\Backend\Bookmark\BookmarkRepository
\TYPO3\CMS\Backend\Controller\ShortcutController
Renamed to \TYPO3\CMS\Backend\Controller\BookmarkController
\TYPO3\CMS\Backend\Backend\ToolbarItems\ShortcutToolbarItem
Renamed to \TYPO3\CMS\Backend\Backend\ToolbarItems\BookmarkToolbarItem

The JavaScript module @typo3/backend/toolbar/shortcut-menu has been removed and replaced by the new bookmark management modules in @typo3/backend/bookmark/ .

14.1 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Feature: #107088 - Allow to utilize password validators in Install Tool 

See forge#107088

Description 

The TYPO3 Install Tool password can now utilize validators as defined via the $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['installTool']['validators'] array. By default, this re-uses the default validator \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator with the configuration:

'SYS' => [
    'passwordPolicies' => [
        'installTool' => [
            'validators' => [
                \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator::class => [
                    'options' => [
                        'minimumLength' => 8,
                        'upperCaseCharacterRequired' => true,
                        'lowerCaseCharacterRequired' => true,
                        'digitCharacterRequired' => true,
                        'specialCharacterRequired' => true,
                    ],
                    'excludeActions' => [],
                ],
            ],
        ],
    ],
],
Copied!

This will require 8 characters minimum (as it was before) and now also require at least one upper-case, one lower-case, one digit and one special character.

The validator is utilized in both scenarios when setting the Install Tool password via CLI (bin/typo3 install:password:set) as well as the Install Tool GUI via Admin Tools > Settings > Change Install Tool Password in the TYPO3 backend.

If a password is auto-generated via the mentioned CLI command, by default it uses 8 characters. The password-length for adapted validator configurations can then be specified with the new --password-length=XXX argument.

Impact 

Maintainers now need to set secure Install Tool passwords and can configure validation rules.

Existing Install Tool passwords are not affected, making this a non-breaking feature. However, these should be revisited by maintainers and maybe set to a more secure password.

To disable password policies (not recommended!), the configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['installTool']['validators'] can be set to an empty array (or null).

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', glob: true }

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.

Feature: #108431 - Add TCA datetime format=datetimesec option 

See forge#108431

Description 

The TCA configuration config option type=datetime can now specify the format=datetimesec format to offer a date/time picker for entering a date (day, month, year) with a specific time (hour, minute, second).

Previously, only a datepicker for hour and minute was available, even though the utilized component (Flatpickr) supports entering seconds.

This format can either be specified for dbType=datetime (native SQL datetime columns based on a timestamp that always includes seconds) or for the integer-based storage without a dbType option (UNIX timestamp).

Example TCA configuration:

EXT:my_extension/Configuration/TCA/tx_domain_model_myelement.php
<?php

return [
    // ...
    'columns' => [
        'meteor_impact' => [
            'label' => 'Time of estimated impact',
            'config' => [
                'type' => 'datetime',
                'format' => 'datetimesec',
                'dbType' => 'datetime', // can also be omitted for integer-based storage
                'nullable' => true, // can also be false
            ],
        ],
    ],
    // ...
];
Copied!

Impact 

Editors and integrators can now specify dates including seconds in database record fields, if the fields are configured with the new TCA config format datetimesec.

This can be set for any TCA field that already internally receives a UNIX timestamp value.

-- For the editor who has everything, but seconds.

Feature: #108462 - Add PSR-14 Event AfterPageContentPreviewRenderedEvent 

See forge#108462

Description 

The class \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem is the central entity to generate various previews of content elements.

Developers can either use the event \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent to generate a preview or implementing \TYPO3\CMS\Backend\Preview\PreviewRendererInterface .

The new PSR-14 event \TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent can now be used to enrich the output generated by one of those.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('my-extension')]
final class AfterPageContentPreviewRenderedEventListener
{
    public function __invoke(AfterPageContentPreviewRenderedEvent $event): void
    {
        $content = 'before<hr />'. $event->getPreviewContent() . '<hr />after';
        $event->setPreviewContent($content);
    }
}
Copied!

Impact 

As integrator it is sometimes handy to enrich the previews of other content elements and plugins to display additional fields.

Feature: #108463 - User-specific configuration of date-timepicker's first day of a week 

See forge#108463

Description 

Previously, the date-time picker used the selected locale of a backend user.

However, certain locale configurations might rather be a user preference. For example, users may prefer an english backend but want to use a weekday start on "Monday" instead of "Sunday" due to their cultural habits.

Thus, the "first day of a week" has been decoupled from the locale selection and can be configured on a per-user setting. By default, it still inherits the locale's default setting, if not changed (for example, english=sunday and german=monday).

Inside the user settings and tab panel Backend appearance, a new dropdown First day of week in calendar popups appears.

The setting is stored in both the persisted be_users.uc preference blob, as well as on the JavaScript-persistence side.

Impact 

Editors can now choose the first day of a week as a user preference, independent from locale selection.

Feature: #108508 - Fluid components integration 

See forge#108508

Description 

Fluid 4.3 introduced the concept of components to Fluid (see Components). Since then, it was already possible to use components in TYPO3 projects by creating a custom ComponentCollection class that essentially connects a folder of template files to a Fluid ViewHelper namespace. Using that class it was also possible to use an alternative folder structure for a component collection and to allow passing arbitrary arguments to components within that collection.

Now it is possible to define component collections purely with configuration. For the most common use cases, it is no longer necessary to create a custom PHP class, which makes it much easier for integrators to setup components in TYPO3 projects.

Registering component collections 

The new extension-level configuration file Configuration/Fluid/ComponentCollections.php is introduced, which allows extensions to register one or multiple new component collections. It is also possible to extend existing collections registered by other extensions (such as adding template paths to override components defined by another extension).

Basic example:

EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'MyVendor\\MyExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:my_extension/Resources/Private/Components',
        ],
    ],
];
Copied!

Components in that collection can then be used in any Fluid template:

<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<my:organism.header.navigation />
Copied!

By default, component collections use a folder structure that requires a separate folder per component. This is handy if you want to put other files right next to your component template, such as the matching CSS or JS file, or even a custom language file. Using the example above, <my:organism.header.navigation /> would point to EXT:my_extension/Resources/Private/Components/Organism/Header/Navigation/Navigation.fluid.html.

If not otherwise specified, components use a strict API, meaning that all arguments that are passed to a component need to be defined with <f:argument> in the component template.

Both defaults can be adjusted per collection by providing configuration options:

  • templateNamePattern allows you to use a different folder structure, available variables are {path} and {name}. For <my:organism.header.navigation> , {path} would be Organism/Header and {name} would be Navigation.
  • setting additionalArgumentsAllowed to true allows passing undefined arguments to components.

Advanced example:

EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'MyVendor\\MyExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:my_extension/Resources/Private/Components',
        ],
        'templateNamePattern' => '{path}/{name}',
        'additionalArgumentsAllowed' => true,
    ],
];
Copied!

Using this example <my:organism.header.navigation /> would point to EXT:my_extension/Resources/Private/Components/Organism/Header/Navigation.fluid.html (note the missing Navigation folder).

It is possible to influence certain aspects of Fluid components using PSR-14 events, see PSR-14 events for Fluid components

Creating components 

A typical component looks just like a normal Fluid template, except that it defines all of its arguments with the Argument ViewHelper <f:argument>. Also, the Slot ViewHelper <f:slot> can be used to receive HTML content.

Example:

EXT:my_extension/Resources/Private/Components/Molecule/TeaserCard/TeaserCard.fluid.html
<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<f:argument name="title" type="string" />
<f:argument name="link" type="string" />
<f:argument name="icon" type="string" optional="{true}" />

<a href="{link}" class="teaserCard">
    <f:if condition="{icon}">
        <my:atom.icon identifier="{icon}">
    </f:if>
    <div class="teaserCard__title">{title}</div>
    <div class="teaserCard__content"><f:slot /></div>
</a>
Copied!

The example also demonstrates that components can (and should) use other components, in this case <my:atom.icon> . Depending on the use case, it might also make sense to pass the output of one component to another component via a slot:

<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<my:molecule.teaserCard
    title="TYPO3"
    link="https://typo3.org/"
    icon="typo3"
>
    <my:atom.text>{content}</my:atom.text>
</my:molecule.teaserCard>
Copied!

You can learn more about components in Defining Components. Note that this is part of the documentation of Fluid Standalone, which means that it doesn't mention TYPO3 specifics.

Migration and co-existence with class-based collections 

Configuration-based and class-based component collections can be used side by side. For more advanced use cases, it might still be best to ship a custom class to define a component collection. However, most use cases can easily be migrated to the configuration-based approach, since they usually just consist of boilerplate code around the configuration options.

Since the new approach is not available in TYPO3 13, it is possible to ship both variants to provide backwards-compatibility: If a specific component collection is defined both via class and via configuration, in TYPO3 13 the class will be used, while in TYPO3 14 the configuration will be used and the class will be ignored completely.

Extending component collections from other extensions 

It is possible to extend the configuration of other extensions using the introduced configuration file. This allows integrators to merge their own set of components into an existing component collection:

EXT:vendor_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'SomeVendor\\VendorExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:vendor_extension/Resources/Private/Components',
        ],
    ],
];
Copied!
EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'SomeVendor\\VendorExtension\\Components' => [
        'templatePaths' => [
            1765990741 => 'EXT:my_extension/Resources/Private/Extensions/VendorExtension/Components',
        ],
    ],
];
Copied!

For template paths, the familiar rule applies: They will be sorted by their keys and will be processed in reverse order. In this example, if my_extension defines a component that already exists in vendor_extension, it will override the original component in vendor_extension.

Impact 

Fluid component collections no longer need to be defined by creating a custom class, but can now be registered purely by configuration. Existing class-based collections will continue to work. If a collection namespace is registered both by a class and by configuration, the configuration overrules the class and any custom code in the class is ignored.

Feature: #108508 - PSR-14 events for Fluid components 

See forge#108508

Description 

Three PSR-14 events have been added to influence the processing and rendering of Fluid components that are registered using the new configuration file (see Fluid components integration).

ModifyComponentDefinitionEvent 

The ModifyComponentDefinitionEvent can be used to modify the definition of a component before it's written to the cache. Component definitions must not have any dependencies on runtime information, as they might be used for static analysis or IDE auto-completion. Due to the component definitions cache, this is already enforced, as the registered events are only executed once and not on every request.

Example:

EXT:my_extension/Classes/EventListener/ModifyComponentDefinitionListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyComponentDefinitionEvent;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinition;
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;

#[AsEventListener]
final readonly class ModifyComponentDefinitionListener
{
    public function __invoke(ModifyComponentDefinitionEvent $event): void
    {
        // Add required argument to one specific component
        if (
            $event->getNamespace() === 'MyVendor\\MyExtension\\Components' &&
            $event->getComponentDefinition()->getName() === 'myComponent'
        ) {
            $originalDefinition = $event->getComponentDefinition();
            $event->setComponentDefinition(new ComponentDefinition(
                $originalDefinition->getName(),
                [
                    ...$originalDefinition->getArgumentDefinitions(),
                    'myArgument' => new ArgumentDefinition('myArgument', 'string', '', true),
                ],
                $originalDefinition->additionalArgumentsAllowed(),
                $originalDefinition->getAvailableSlots(),
            ));
        }
    }
}
Copied!

ProvideStaticVariablesToComponentEvent 

The ProvideStaticVariablesToComponentEvent can be used to inject additional static variables into component templates. As with the ModifyComponentDefinitionEvent , these variables must not have any dependencies on runtime information, as they might be used for static analysis or IDE auto-completion. The RenderComponentEvent can be used to add variables with runtime dependencies.

Valid use cases for this event might be:

  • providing static (!) design tokens (colors, icons, ...) to all components in a collection
  • generating prefix strings based on the component's name

Example:

EXT:my_extension/Classes/EventListener/ProvideStaticVariablesToComponentListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ProvideStaticVariablesToComponentEvent;

#[AsEventListener]
final readonly class ProvideStaticVariablesToComponentListener
{
    public function __invoke(ProvideStaticVariablesToComponentEvent $event): void
    {
        // Provide design tokens to all components in a collection
        if ($event->getComponentCollection()->getNamespace() === 'MyVendor\\MyExtension\\Components') {
            $event->setStaticVariables([
                ...$event->getStaticVariables(),
                'designTokens' => [
                    'color1' => '#abcdef',
                    'color2' => '#123456',
                ],
            ]);
        }
    }
}
Copied!

RenderComponentEvent 

The RenderComponentEvent can be used to alter or replace the rendering of Fluid components. There are three possible use cases:

  1. fully take over the rendering of components by filling the $renderedContent with $event->setRenderedContent() . The first event that does this skips all following event listeners.
  2. provide additional arguments (= variables in the component template) or slots to the component with $event->setArguments() / $event->setSlots() .
  3. execute additional code that doesn't influence the component rendering directly, e. g. adding certain frontend assets to the page automatically.

Example:

EXT:my_extension/Classes/EventListener/RenderComponentListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Fluid\Event\RenderComponentEvent;

#[AsEventListener]
final readonly class RenderComponentListener
{
    public function __construct(private AssetCollector $assetCollector) {}

    public function __invoke(RenderComponentEvent $event): void
    {
        // Add bundled components CSS if a component is used on the page
        if ($event->getComponentCollection()->getNamespace() === 'MyVendor\\MyExtension\\Components') {
            $this->assetCollector->addStyleSheet(
                'componentsBundle',
                'EXT:my_extension/Resources/Public/ComponentsBundle.css'
            );
        }
    }
}
Copied!

Impact 

Three new PSR-14 events can be used to influence the processing and rendering of Fluid components.

Feature: #108524 - Configuration file to register global Fluid namespaces 

See forge#108524

Description 

The extension-level configuration file Configuration/Fluid/Namespaces.php is introduced, which enables a structured way to register and extend global Fluid namespaces. This replaces the old configuration in $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] , see deprecation.

Example:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
    'mycmp' => ['MyVendor\\MyExtension\\Components'],
];
Copied!

Overriding existing ViewHelpers 

TYPO3 reads and merges Configuration/Fluid/Namespaces.php files from all loaded extensions in the usual loading order, which can be manipulated by declaring dependencies in composer.json and possibly ext_emconf.php. If an extension registers a namespace that has already been registered by another extension, these namespaces will be merged by Fluid. This allows extensions to override ViewHelpers of another extension selectively.

Example (extension2 depends on extension1):

EXT:my_extension1/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension1\\ViewHelpers'],
];
Copied!
EXT:my_extension2/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension2\\ViewHelpers'],
];
Copied!

Resulting namespace definition:

[
    'myext' => [
        'MyVendor\\MyExtension1\\ViewHelpers',
        'MyVendor\\MyExtension2\\ViewHelpers',
    ],
];
Copied!

Namespaces are processed in reverse order, which means that <myext:demo /> would first check for EXT:my_extension2/Classes/ViewHelpers/DemoViewHelper.php, and would fall back to EXT:my_extension1/Classes/ViewHelpers/DemoViewHelper.php.

PSR-14 event to modify namespaces 

The new ModifyNamespacesEvent is introduced, which allows modification of the whole namespaces array before it is being passed to Fluid. This allows for example to:

  • completely redefine an existing namespace (instead of extending it)
  • add namespaces conditionally
  • modify order of merged namespaces

Example:

EXT:my_extension/Classes/EventListener/ModifyNamespacesListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyNamespacesEvent;

#[AsEventListener]
final readonly class ModifyNamespacesListener
{
    public function __invoke(ModifyNamespacesEvent $event): void
    {
        $namespaces = $event->getNamespaces();
        // Replace existing "theme" namespace completely
        $namespaces['theme'] = ['MyVendor\\MyExtension\\ViewHelpers'];
        $event->setNamespaces($namespaces);
    }
}
Copied!

Note that namespaces might still be imported locally from within a template file, which is unaffected by this event.

Backwards-compatible namespaces in extensions 

There are several ways to provide backwards-compatible global namespaces in extensions, depending on the concrete use case:

  • preferred: Define namespace in TYPO3_CONF_VARS (with version check) and in new Namespaces.php. This means that in TYPO3 v14 installations the new Namespaces.php can already be used to extend the namespace.
  • alternative: Define namespace both in TYPO3_CONF_VARS (without version check) and in new Namespaces.php. This means however that the namespace can only be extended with TYPO3_CONF_VARS, not with the new Namespaces.php.
  • keep TYPO3_CONF_VARS until support for < v14 is dropped by the extension. This can only be extended with TYPO3_CONF_VARS as well.
  • implement own merging logic in ModifyNamespacesEvent if necessary.

Example for preferred option:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
];
Copied!
EXT:my_extension/ext_localconf.php
<?php

if ((new \TYPO3\CMS\Core\Information\Typo3Version())->getMajorVersion() < 14) {
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['myext'][] = 'MyVendor\\MyExtension\\ViewHelpers';
}
Copied!

To sum up:

  • Namespaces.php can only extend namespaces defined in Namespaces.php.
  • TYPO3_CONF_VARS can extend both TYPO3_CONF_VARS and Namespaces.php.
  • ModifyNamespacesEvent can modify everything.

Impact 

Extensions can now register global Fluid namespaces in a dedicated configuration file Configuration/Fluid/Namespaces.php. The old TYPO3_CONF_VARS registration can be used for backwards compatibility.

Feature: #108539 - Introduce default theme "Camino" 

See forge#108539

Description 

A new default theme has been added. Its main purpose is to build new sites more rapidly in TYPO3 v14.

The name Camino (spanish "the way") was chosen as v14 development series, marking the first steps on the way to a glorious future for TYPO3.

It serves to show that a new site in TYPO3 can be set up within minutes, being customizable (at least in a limited way), without having to rely on any external library, and to avoid ANY error message for newcomers to TYPO3.

Camino will be packaged for new installations by default and can be activated for new sites alongside existing sites.

The theme shows off basic page structures as well as some default content elements - completely without any third-party dependencies nor requiring the "Fluid styled content" extension.

The rough structure of the theme:

  • Four different color schemes can be selected in the site settings
  • The main menu structure and footer structure can be configured on the root page inside the backend layout / colPos positions
  • Common content elements for a Hero area and regular content is available
  • Minimal configuration is handled in TypoScript

The theme is not meant to evolve within TYPO3, as it will be moved to TER/Packagist/GitHub in a separate repository in v15.0. In v15.x a new theme will be added with more modern features, and it will utilize features that will be added during v15.x development.

The theme is 100% optional and encapsulated - existing setups will have no interference.

The theme will be fine-tuned before the TYPO3 v14 LTS release, specific documentation for its features will be provided in the theme's documentation.

Installation 

For now, the theme is the same as a regular TYPO3 extension.

On fresh classic-mode installations, the theme will be enabled by default. A new site and a first page will be created, which can be used to insert content.

On Composer-mode installations, the package typo3/theme-camino needs to be required, and a fresh installation will also create the site and a first page.

For existing installations, the theme must be enabled first (depending on the TYPO3 setup) either via extension manager, or by requiring the Composer package.

Once the "extension" is activated, the steps to enable the Camino frontend are:

  • Create a new root page in the Content > Layout page tree. Be sure to edit the created page properties and enable Behavior > Use as Root Page .
  • This will automatically create a new Site. Check Sites > Setup to see the created Site. Edit that Site's properties. In General > Sets for this Site ensure that the Theme: Camino Site set is added as a dependency.
  • (Depending on the TYPO3 setup, TYPO3 caches might need to be cleared)
  • Then edit the created root page properties via Content > Layout again, and pick Camino: Start page from the tab Appearance > Backend Layout (this page only).
  • Now the Camino theme will be applied to the site. Content can be added in the specific columns, and sub-pages can be created (ensure to set the backend layout of subpages to the appropriate Camino backend layout, either Camino: Content page (full-width) or Camino: Content page (with sidebar).
  • A custom logo can be set in root page properties Appearance, just above the backend layout picker.
  • In Sites > Setup, the Site set configuration can be accessed to adjust the color scheme and further options.

Impact 

A default frontend theme is now available. It can be easily activated in the TYPO3 installation process, or also be enabled afterwards.

It is dependency-free and provides and utilizes site sets.

Feature: #108558 - Add original file name to SanitizeFileNameEvent 

See forge#108558

Description 

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent does now also provide the original file name. Event listeners can use the original file name to perform custom string replacements (e.g. space to hyphen instead of underscore) for the sanitized file name.

Impact 

It is now possible to retrieve the original file name in the PSR-14 event \TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent .

Feature: #108623 - Allow content element restrictions per colPos 

See forge#108623

Description 

Backend layouts have been extended with options to allow only configured types of content elements (referencing tt_content.CType with names like "text", "textmedia", "felogin_pi1" and so on) in backend layout columns ( colPos ): The two keys allowedContentTypes and disallowedContentTypes add allow and deny lists on column level. These settings can be set with Page TSConfig based backend layouts, Database based backend layouts do not allow configuring this value at the moment, but this will be added soon.

Example for a backend layout with two rows and two columns configured using Page TSConfig:

mod.web_layout.BackendLayouts {
  exampleKey {
    title = Example
    config {
      backend_layout {
        colCount = 1
        rowCount = 2
        rows {
          1 {
            columns {
              1 {
                identifier = main
                name = Main content
                colPos = 0
                allowedContentTypes = header, textmedia
              }
              2 {
                identifier = right
                name = Panel right
                colPos = 1
                allowedContentTypes = my_custom_cta
              }
            }
          }
          2 {
            columns {
              1 {
                identifier = footer
                name = Footer
                colpos = 2
                colspan = 2
                disallowedContentTypes = header
            }
        }
      }
    }
  }
}
Copied!

The implementation adapts the "New content element wizard" to show only allowed (or not disallowed) content elements types when adding a content element to a column. When editing records, the select boxes "Type" and Column position" are reduced to not allow invalid values based on the configuration. Similar logic is applied when moving and copying content elements.

The feature has been created with extension content_defender in mind. This extension by Nicole Hummel has been around for many years and found huge adoption rates within the community. In comparison to content_defender, the core configuration is slightly simplified and the core implementation does not provide the additional content_defender feature to restrict the number of elements per column ( maxitems ).

The core implementation supports the content_defender syntax using the arrays allowed.CType and disallowed.CType . With the example below, allowed.CType is internally mapped to allowedContentTypes . When both allowed.CType and allowedContentTypes are given, allowed.CType is ignored.

mod.web_layout.BackendLayouts {
  exampleKey {
    config {
      backend_layout {
        rows {
          1 {
            columns {
              1 {
                allowed {
                  CType = header, textmedia
[...]
Copied!

Codewise, the PSR-14 event ManipulateBackendLayoutColPosConfigurationForPageEvent has been added. It allows manipulation of the calculated column configuration. It is marked @internal and thus needs to be used with care since it may change in the future without further note: The event is not dispatched as systematically as it should be, but refactoring the surrounding code can probably not be provided with TYPO3 v14 anymore. Extensions like ext:container however must be able to adapt column configuration with TYPO3 v14 already. The decisions was to provide an event, but to mark it internal for the time being, declaring it as "use at your own risk" if you know what you are doing and within extensions that set up proper automatic testing to find issues if the core changes internals.

Impact 

Backend layout columns can now restrict, which content element types are allowed or disallowed inside of it.

Feature: #108627 - Allow adding inline language domains to JavaScript 

See forge#108627

Description 

The new method PageRenderer->addInlineLanguageDomain() allows loading all labels from a language domain and making them available in JavaScript via the TYPO3.lang object.

The domain name follows the format extension.domain (e.g. core.common, core.modules.media). The language file is resolved automatically by the LanguageService, resolving to files like EXT:core/Resources/Private/Language/locallang_common.xlf and EXT:core/Resources/Private/Language/Modules/media.xlf.

See translation domain syntax for more details.

Labels are automatically prefixed with the domain name and accessible as TYPO3.lang['domain:key'] , e.g. TYPO3.lang['core.common:notAvailableAbbreviation'] .

Example 

EXT:my_extension/Classes/Controller/MyController.php
use TYPO3\CMS\Core\Page\PageRenderer;

final class MyController
{
    public function __construct(
        private readonly PageRenderer $pageRenderer,
    ) {}

    public function myAction(): void
    {
        // Load all labels from the 'myextension.frontend' domain
        $this->pageRenderer->addInlineLanguageDomain('myextension.frontend');
    }
}
Copied!

The labels are then available in JavaScript:

EXT:my_extension/Resources/Public/JavaScript/my-script.js
// Access a label from the domain
const label = TYPO3.lang['myextension.frontend:button.submit'];
Copied!

Impact 

Previously, it was possible to load entire language files using addInlineLanguageLabelFile() (which is still available), but labels were added without any prefix. This could lead to naming conflicts when multiple extensions used the same label keys, potentially overriding each other's translations.

With the new domain-based approach, all labels are automatically prefixed with the domain name (e.g. myextension.frontend:label.key). This provides a unified, namespaced access pattern that eliminates the risk of collisions between labels from different extensions or language files.

Feature: #108663 - Adjust visibility of form elements in Form Editor 

See forge#108663

Description 

The Form Editor now provides the ability to configure the visibility of form elements. This allows form administrators to control which form elements are displayed or hidden in the form, providing better control over the form structure and user experience.

Usage 

When editing a form element in the Form Editor, a new "Visibility" option is available in the element's configuration panel. This option allows you to:

  • Show the element (default behavior)
  • Hide the element

The visibility setting is stored in the form definition and is evaluated when the form is rendered on the frontend.

Example for Integrators 

If you want to extend your own form elements with the visibility feature, you need to add the following configuration to the element definition:

prototypes:
  standard:
    formElementsDefinition:
      CustomElement:
        formEditor:
          editors:
            # Choose a key / position according to your own needs
            240:
              identifier: enabled
              templateName: Inspector-CheckboxEditor
              label: formEditor.elements.FormElement.editor.enabled.label
              propertyPath: renderingOptions.enabled
Copied!

Impact 

This feature improves the usability of the Form Editor and makes it more accessible to non-technical users who need to manage form visibility.

Feature: #108694 - Context menu items to edit site configuration for pages 

See forge#108694

Description 

Two new context menu items have been added for pages that are site roots:

  • Edit Site: Opens the site configuration editor for the site associated with the page.
  • Edit Site Settings: Opens the site settings editor for the site.

These items appear directly after the "Edit" item in the context menu and are only visible to admin users on pages that have a site configuration.

Impact 

Admin users can now quickly access the site configuration and site settings directly from the context menu when right-clicking on a site root page. This improves the workflow for managing sites without needing to navigate to the Site Management module first.

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.

Deprecation: #108524 - Fluid namespaces in TYPO3_CONF_VARS 

See forge#108524

Description 

Registering global namespaces for Fluid templates in TYPO3_CONF_VARS has been deprecated.

Impact 

Defined namespaces in $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] will no longer be registered in TYPO3 v15.

Affected installations 

Installations and extensions that use $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] to define additional global namespaces or extend existing global namespaces.

Migration 

Standard use cases, such as registering a new global namespace or extending an existing one, can be migrated to the dedicated Configuration/Fluid/Namespaces.php configuration file.

Before:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['myext'][] = 'MyVendor\\MyExtension\\ViewHelpers';
Copied!

After:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
];
Copied!

See Feature: #108524 - Configuration file to register global Fluid namespaces for more details and examples.

Deprecation: #108667 - Deprecate CommandNameAlreadyInUseException 

See forge#108667

Description 

The exception \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException is unused within TYPO3 Core and has been deprecated.

Impact 

Creating a new instance of \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException will trigger a PHP deprecation message.

Affected installations 

TYPO3 installations with custom extensions using this exception.

Migration 

As the exception is unused in TYPO3 Core, there is no direct replacement. Extensions relying on this exception should implement their own exception if needed.

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: #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(36) 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: #104422 - Move GET parameters in sitemap into namespace 

See forge#104422

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: #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. Such comma-separated keys were used to address flex form fields in page TSconfig overrides via TCEFORM.<table>.<field>.<dataStructureKey> and in backend user exclude-field definitions via <table>:<field>;<dataStructureKey>. This fallback emits a PHP deprecation notice and is removed in TYPO3 v15, so page TSconfig and exclude-field addressing then use the data structure key as-is (the record type value, for example textpic).

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 TYPO3CMSCoreDataHandlingDataHandler 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 - Fluid 5.0 

See forge#108148

Description 

Fluid 5.0 removes pre-announced deprecations that were introduced with Fluid 2.x and 4.x.

Impact 

Installations that use methods that were deprecated with Fluid 2.x or Fluid 4.x will now encounter PHP errors.

Affected installations 

Breaking changes are listed in the Fluid documentation:

Changelog 5.x

Migration 

Noteworthy deprecations and mitigations have already been communicated with TYPO3 changelog entries in 13.x:

Deprecation items in Fluid changelogs might contain additional hints:

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.

For template files already using the new *.fluid.* file extension, the built-in template analyse command will discover affected template files:

vendor/bin/typo3 fluid:analyse
Copied!

For each affected template, the command will output an error like this:

[ERROR] packages/myext/Resources/Private/Templates/Test.fluid.html: Variable identifiers cannot start with a "_": _myvariable
Copied!

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
  • styleguide:generate

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: