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, 05 Jun 2026 10:47:23 +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:

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 classes have been declared final:

  • \TYPO3\CMS\SomeExtension\Some\ClassName

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 interfaces changed:

  • \TYPO3\CMS\SomeExtension\Some\InterfaceName->someMethod() added

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\ContentObjectRenderer->parentRecordNumber (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 property has changed/enforced type:

  • \TYPO3\CMS\SomeExtension\Some\ClassName->someProperty (is now \Some\Type)

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 global variables have been changed:

  • $GLOBALS['TYPO3_CONF_VARS']['SOME']['option'] description of change

The following hooks have been removed:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['some']['hook']

The following extension file loading has been removed:

  • ext_tables.php files in extensions are no longer considered during bootstrap (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 validator options have been removed:

  • someOption in \TYPO3\CMS\Extbase\Validation\Validator\SomeValidator

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)
  • 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 row updater has been removed:

  • \TYPO3\CMS\Install\Updates\RowUpdater\SomeMigration

The following database table fields have been removed:

  • some_table.some_field

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 smooth migration for JavaScript modules have been removed:

  • @typo3/some-extension/old-module to @typo3/some-extension/new-module

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.

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 content element definitions have been removed:

  • tt_content.some_element

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: #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!

ChangeLog v14 

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

Also available 

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!

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.

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: 'bar',
    baz: 'bencer',
});
console.log(urlSearchParams.toString());
// foo=bar&baz=bencer
Copied!

Feature: #107105 - Introduce expression for accessing site locale 

See forge#107105

Description 

A new Symfony ExpressionLanguage expression locale() has been introduced.

This expression allows integrators and developers to directly access the current site locale, which is provided as a locale object. All public methods of this object are available for use.

For more information, refer to the API documentation: API

Example 

Using locale() in TypoScript conditions
[locale().getName() == "en-US"]
    page.20.value = bar
[END]
[locale().getCountryCode() == "US"]
    page.30.value = foo
[END]
[locale().isRightToLeftLanguageDirection()]
    page.40.value = bar
[END]
Copied!
Using locale() in a form variant definition
variants:
  - identifier: language-variant-1
    condition: 'locale().getName() == "en-US"'
    label: 'First name'
Copied!

Impact 

Developers can now compare or evaluate the site locale directly in expressions, without using siteLanguage("locale").

Feature: #107151 - Add AsNonSchedulableCommand attribute for CLI commands 

See forge#107151

Description 

With forge#101567 the usage of Symfony's #[AsCommand] attribute has been introduced, which allows configuring a Symfony CLI command with a corresponding name, description and further options.

It however lacked TYPO3's custom implementation of the schedulable option, which allows flagging a CLI command to be not allowed to be scheduled via the Administration > Scheduler backend module.

This previously required tagging such a command with the schedulable: false tag attribute in the Services.yaml or Services.php definition.

For this, the PHP attribute AsNonSchedulableCommand has been introduced. Any Symfony Command can use this empty attribute. The automatic Scheduler registry will ignore any command with this tag.

By default, a Symfony Command remains schedulable using the regular Symfony attribute. To prevent redundancy, the new attribute #[AsNonSchedulableCommand] should be used only on top of that.

Another advantage is that an IDE like PhpStorm is capable of showing all usages of that attribute inside a project.

Impact 

Developers can now fully embrace using the Symfony #[AsCommand] attribute and still be able to declare a non-schedulable execution within the scope of the same class, without any service registration.

This is achieved by using the #[AsNonSchedulableCommand] in addition to the #[AsCommand] attribute.

Example 

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand('myextension:import', 'Import data from external source')]
#[AsNonSchedulableCommand]
final class ImportCommand extends Command
{
    // ...
}
Copied!

Feature: #107180 - PSR-14 events for Backend Users module 

See forge#107180

Description 

Several PSR-14 events have been added to allow customizations of the Backend Users module - in particular, which users, groups, and file mounts can be viewed in the module.

AfterBackendUserListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend user repository fetches a list of filtered backend users (itself called when displaying the list of users in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Model\Demand containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendUserListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(
        AfterBackendUserListConstraintsAssembledFromDemandEvent $event
    ): void {
        $event->constraints[] = $event->query->eq('admin', 1);
    }
}
Copied!

AfterBackendGroupListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend group repository fetches a list of filtered backend groups (itself called when displaying the list of groups in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Dto\BackendUserGroup containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupListConstraintsAssembledFromDemandEvent $event): void
    {
        $event->constraints[] = $event->query->eq('workspace_perms', 1);
    }
}
Copied!

AfterBackendGroupFilterListIsAssembledEvent 

A list of user groups can be used to filter the users list in the backend module. This event is dispatched right after this list is assembled and makes it possible to modify it.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $backendGroups: An array of backend groups.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupFilterListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupFilterListIsAssembledEvent $event): void
    {
        array_pop($event->backendGroups);
    }
}
Copied!

AfterFilemountsListIsAssembledEvent 

This event is dispatched when the file mounts list is fetched to display in the backend module. It makes it possible to modify this list.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $filemounts: An array of file mounts.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;;

use TYPO3\CMS\Beuser\Event\AfterFilemountsListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterFilemountsListIsAssembledEvent $event): void
    {
        array_pop($event->filemounts);
    }
}
Copied!

Impact 

These events can be used to implement custom user or permission management processes in the Backend Users module. Be aware that this area is security sensitive. Ensure that no unauthorized data exposure or privilege escalation occurs when modifying these queries or lists.

Feature: #107201 - Extended RSS Widget with Atom Support 

See forge#107201

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing RSS widget has been extended to support Atom feeds. This provides a unified solution for both RSS and Atom feed formats within the TYPO3 Dashboard system.

The configurable widget architecture makes it possible to create feed-based widgets that users can configure directly through the dashboard interface. This capability now extends seamlessly to Atom feeds, which are commonly used by platforms such as GitHub for release feeds and project updates.

The RSS widget now automatically detects and parses both RSS and Atom feed formats. Atom feeds are parsed according to the Atom Syndication Format (RFC 4287). This provides comprehensive support for modern feed formats used by many development platforms and services, all within a single widget implementation.

Impact 

  • The existing "RSS Widget" now supports both RSS and Atom feeds automatically.
  • Atom feeds (commonly used by GitHub, GitLab, and other platforms) can now be displayed using the same RSS widget.
  • Widget instances are configurable with custom labels, feed URLs, and display limits.
  • Each widget can be configured independently, allowing multiple feeds of different formats on the same dashboard.
  • Automatic caching ensures optimal performance with configurable cache lifetimes.

Currently configurable options 

The RSS widget supports the following configuration options for both RSS and Atom feeds:

Label
  • Custom title for the widget instance.
  • Optional field that defaults to the widget’s default title.
Feed URL
  • RSS or Atom feed URL to display.
  • Format detection is automatic based on feed content.
  • Can be preconfigured in the service definition to create read-only instances.
Limit
  • Number of feed entries to show (default: 5).
  • Configurable integer value to control widget content density.
Lifetime
  • Cache duration in seconds for the feed (default: 43200 = 12 hours).
  • Advanced setting typically configured by integrators.
  • Not configurable in the user interface.

Example for RSS widget with Atom feed 

Service configuration

EXT:my_extension/Configuration/Services.yaml
services:

  # Button provider for external link
  dashboard.buttons.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
    arguments:
      $title: 'View all releases'
      $link: 'https://github.com/TYPO3/typo3/releases'
      $target: '_blank'

  # RSS widget with Atom feed URL
  dashboard.widget.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
    arguments:
      $buttonProvider: '@dashboard.buttons.github_releases'
      $options:
        feedUrl: 'https://github.com/TYPO3/typo3/releases.atom'
        lifeTime: 43200
        limit: 10
    tags:
      - name: dashboard.widget
        identifier: 'github_releases'
        groupNames: 'general'
        title: 'my_extension.widgets:github_releases.title'
        description: 'my_extension.widgets:github_releases.description'
        iconIdentifier: 'content-widget-rss'
        height: 'large'
        width: 'medium'
Copied!

Usage in the dashboard

  1. Navigate to the dashboard where you want to add the widget.
  2. Click Add widget and select the RSS widget.
  3. Click the settings (cog) icon to customize the widget.
  4. Configure the feed URL (RSS or Atom), limit, and label as needed.
  5. Save the configuration to apply the changes.

The widget automatically detects the feed format and displays entries with titles, publication dates, content summaries, and author information when available.

Feed format support 

The RSS widget now supports both feed formats:

RSS feeds
Item titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Descriptions
displayed as entry content (HTML tags stripped).
Atom feeds
Entry titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Content/Summary
displayed as entry description (HTML tags stripped).
Author information
name, email, and URL when provided in the feed.

Feature: #107240 - Add warning when pasting password with whitespace 

See forge#107240

Description 

A new warning mechanism has been introduced in the backend login form to help users avoid authentication issues when pasting passwords that contain leading or trailing whitespace.

When a password is pasted into the backend login form, TYPO3 now detects if the pasted text contains leading or trailing whitespace characters (spaces, tabs, newlines, etc.) and displays a warning message to the user.

The warning includes an action button that allows users to automatically remove the surrounding whitespace from the pasted password, ensuring successful login attempts.

This feature helps prevent common login failures caused by accidentally copying whitespace along with passwords from password managers, text editors, or other sources.

The implementation includes:

  • Detection of leading and trailing whitespace in pasted passwords
  • Visual warning message displayed to the user
  • One-click action to remove the surrounding whitespace

Example 

The whitespace detection covers various whitespace characters including:

  • Regular spaces (U+0020)
  • Tabs (U+0009)
  • Line breaks (U+000A, U+000D)
  • Other Unicode whitespace characters

Impact 

Users now receive immediate feedback when pasting passwords that contain surrounding whitespace, reducing login failures and improving the overall user experience when authenticating to the TYPO3 backend.

Feature: #107256 - PSR-14 event to modify options in EmailFinisher 

See forge#107256

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent has been introduced. This event is dispatched before the \EmailFinisher is initialized and allows listeners to modify the finisher options dynamically.

This enables developers to customize email behavior programmatically, such as:

  • Setting alternative recipients based on frontend user permissions
  • Modifying the email subject or content dynamically
  • Replacing recipients with developer email addresses in test environments
  • Adding or removing CC or BCC recipients conditionally
  • Customizing reply-to addresses

The event provides access to both the finisher context (read-only) and the options array, allowing for flexible manipulation of the email configuration.

To modify the \EmailFinisher options, the following methods are available:

  • getFinisherContext(): Returns the FinisherContext containing form runtime and request information
  • getOptions(): Returns the current finisher options array
  • setOptions(): Allows setting the modified options array

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

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

final class BeforeEmailFinisherInitializedEventListener
{
    #[AsEventListener('my_extension/form/modify-email-finisher-options')]
    public function __invoke(BeforeEmailFinisherInitializedEvent $event): void
    {
        $options = $event->getOptions();
        $context = $event->getFinisherContext();

        // Overwrite recipients based on FormContext
        if ($context->getFormRuntime()->getFormDefinition()->getIdentifier() === 'my-form-123') {
            $options['recipients'] = ['user@example.org' => 'John Doe'];
        }

        // Modify subject dynamically
        $options['subject'] = 'Custom subject: ' . ($options['subject'] ?? '');

        // Clear CC and BCC recipients
        $options['replyToRecipients'] = [];
        $options['blindCarbonCopyRecipients'] = [];

        $event->setOptions($options);
    }
}
Copied!

Impact 

It is now possible to dynamically modify \EmailFinisher options before email processing begins, using the new PSR-14 event BeforeEmailFinisherInitializedEvent . This provides developers with full control over email configuration without needing to extend or override the \EmailFinisher class.

Feature: #107281 - Type-specific TCAdefaults support 

See forge#107281

Description 

The TCAdefaults configuration has been extended to support type-specific syntax similar to TCEFORM, enabling different default values based on the record type.

This allows configuration like:

Page TSconfig
# Field-level default (applies to all content types)
TCAdefaults.tt_content.header_layout = 1

# Type-specific defaults (applies only to specific content types)
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.frame_class.types.textmedia = ruler-before
Copied!

The same syntax is supported in User TSconfig as well.

Type-specific defaults take precedence over field-level defaults, and Page TSconfig overrides User TSconfig following the established inheritance pattern.

Implementation details 

The feature is implemented in two main areas:

  • Backend forms: The DatabaseRowInitializeNew class now processes type-specific defaults when creating new records in the backend.
  • DataHandler: The DataHandler class supports type-specific defaults when creating records programmatically via the PHP API.

Fallback behavior 

If no type-specific default is found for a given record type, the system falls back to:

  1. Field-level TCAdefaults configuration
  2. TCA 'default' configuration
  3. Database field default value

Automatic field discovery 

This enhancement makes TCAdefaults consistent with TCEFORM patterns and enables automatic field discovery.

Examples 

User TSconfig - Basic type-specific defaults
TCAdefaults.tt_content.header_layout = 1
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.header_layout.types.image = 2
Copied!
Page TSconfig - Multiple fields with type-specific overrides
TCAdefaults.tt_content {
    header_layout = 1
    header_layout.types.textmedia = 3
    header_layout.types.image = 2

    frame_class = default
    frame_class.types.textmedia = ruler-before
    frame_class.types.image = none

    space_before_class = none
}
Copied!
PHP API usage - DataHandler with type-specific defaults
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$datamap = [
    'tt_content' => [
        'NEW123' => [
            'pid' => 42,
            'CType' => 'textmedia',
            'header' => 'My Content Element',
            // header_layout and frame_class will be set automatically
            // based on type-specific TCAdefaults configuration
        ],
    ],
];
$dataHandler->start($datamap, [], $backendUser);
$dataHandler->process_datamap();
Copied!

Impact 

This feature provides a more flexible and consistent way to configure default values for different record types, reducing repetitive configuration and improving the user experience when creating new records.

The type-specific syntax aligns TCAdefaults with the established TCEFORM pattern, making the configuration more intuitive for TYPO3 developers and integrators.

Feature: #107322 - New PSR-14 AfterRichtextConfigurationPreparedEvent 

See forge#107322

Description 

A new PSR-14 event AfterRichtextConfigurationPreparedEvent has been added.

To modify the configuration, the following methods are available:

  • setConfiguration()
  • getConfiguration()

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Configuration\Event\AfterRichtextConfigurationPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class AfterRichtextConfigurationPreparedEventListener
{
    #[AsEventListener('my_extension/configuration/modify-rich-text-configuration')]
    public function __invoke(AfterRichtextConfigurationPreparedEvent $event): void
    {
        $config = $event->getConfiguration();
        $config['editor']['config']['debug'] = true;
        $event->setConfiguration($config);
    }
}
Copied!

Impact 

It is now possible to modify the rich-text configuration after it has been fetched and prepared.

Feature: #107343 - PSR-14 to manipulate form creation process 

See forge#107343

Description 

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

The new event is dispatched immediately before a new form is created in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the new form

Example 

An example event listener could look like:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

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

final class MyEventListener
{
    #[AsEventListener('my_extension/before-form-is-created')]
    public function __invoke(BeforeFormIsCreatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsCreatedEvent , it is now possible to modify a form before it is created.

Feature: #107358 - Introduce Fluid page title ViewHelper 

See forge#107358

Description 

A new Fluid ViewHelper <f:page.title> has been introduced to allow setting the page title directly from Fluid templates.

This is especially useful for Extbase plugins that need to set a page title in their detail views without having to implement a custom page title provider.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.title>{item.title}</f:page.title>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper can also be used with static content:

EXT:my_extension/Resources/Private/Templates/Static/About.html
<f:page.title>About Us - Company Information</f:page.title>

<h1>About Us</h1>
<p>Welcome to our company...</p>
Copied!

Impact 

Extension developers can now easily set page titles from their Fluid templates without creating custom page title providers for each extension. This simplifies the implementation of dynamic page titles in Fluid templates or in Extbase plugins for detail views where the title should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing page title provider system and respects the configured provider priorities.

Feature: #107359 - Introduce Fluid page meta ViewHelper 

See forge#107359

Description 

A new Fluid ViewHelper <f:page.meta> has been introduced to allow setting meta tags directly from Fluid templates using TYPO3's MetaTagManager API.

This is especially useful for Extbase plugins that need to set meta tags in their detail views without having to implement custom meta tag handling.

In addition, frontend developers do not need to write custom TypoScript anymore but can use Fluid directly.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.meta property="description">
    {item.description}
</f:page.meta>
<f:page.meta property="og:title">
    {item.title}
</f:page.meta>
<f:page.meta property="og:type">
    article
</f:page.meta>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper supports all features of the MetaTagManager API:

OpenGraph and Twitter / X Card meta tags:

<f:page.meta property="og:title">
    My Article Title
</f:page.meta>
<f:page.meta property="og:description">
    Article description
</f:page.meta>
<f:page.meta property="twitter:card">
    summary_large_image
</f:page.meta>
Copied!

Sub-properties for complex meta tags:

<f:page.meta
    property="og:image"
    subProperties="{width: 1200, height: 630, alt: 'Article image'}"
>
    {item.image.url}
</f:page.meta>
Copied!

Custom meta tag types:

<f:page.meta property="author" type="name">
    John Doe
</f:page.meta>
<f:page.meta property="robots" type="name">
    index, follow
</f:page.meta>
Copied!

Replacing existing meta tags:

<f:page.meta property="description" replace="true">
    Override any existing description
</f:page.meta>
Copied!

ViewHelper Arguments 

  • property (required): The meta property name (for example, description, og:title)
  • type (optional): The meta type attribute (name, property, http-equiv). If not set, the appropriate MetaTagManager determines the type automatically.
  • subProperties (optional): Array of sub-properties for complex meta tags.
  • replace (optional): Boolean to replace existing meta tags with the same property (default: false).

Impact 

Extension developers can now easily set meta tags from Fluid templates without needing to use the MetaTagManager API directly in PHP code. This simplifies the implementation of SEO-optimized pages in Extbase plugins, especially for detail views where meta tags should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing MetaTagManager system and respects all configured meta tag managers and their priorities.

Feature: #107380 - PSR-14 to manipulate form duplication process 

See forge#107380

Description 

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

The new event is dispatched immediately before a form is duplicated in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the duplicated form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

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

final class BeforeFormIsDuplicatedEventListener
{
    #[AsEventListener('my_extension/before-form-is-duplicated')]
    public function __invoke(BeforeFormIsDuplicatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsDuplicatedEvent , it is now possible to modify a form before it is duplicated.

Feature: #107382 - PSR-14 before form deletion 

See forge#107382

Description 

A new PSR-14 event BeforeFormIsDeletedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] .

The new event is dispatched immediately before a form is deleted in the backend.

The event provides the following public properties:

  • $formPersistenceIdentifier: The form persistence identifier (read-only)
  • $preventDeletion: A boolean flag that can be set to true to prevent the deletion of the form

The new event is stoppable. As soon as $preventDeletion is set to true, no further listener is called.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

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

final class BeforeFormIsDeletedEventListener
{
    #[AsEventListener('my_extension/before-form-is-deleted')]
    public function __invoke(BeforeFormIsDeletedEvent $event): void
    {
        $event->preventDeletion = true;
        $persistenceIdentifier = $event->formPersistenceIdentifier;
        // Do something with the persistence identifier
    }
}
Copied!

Impact 

With the new BeforeFormIsDeletedEvent , it is now possible to prevent the deletion of a form and to add custom logic based on the delete action.

Feature: #107388 - PSR-14 to manipulate form before it is saved 

See forge#107388

Description 

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

The new event is dispatched immediately before a form is saved in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

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

final class BeforeFormIsSavedEventListener
{
    #[AsEventListener('my-extension/before-form-is-saved')]
    public function __invoke(BeforeFormIsSavedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsSavedEvent , it is now possible to modify a form definition as well as the form persistence identifier before it is saved.

Feature: #107435 - File replace modal with enhanced file information 

See forge#107435

Description 

The file replacement functionality has been significantly improved to provide a better user experience in the TYPO3 backend. Instead of opening the file replacement interface in a new window, it now opens in a modal dialog, maintaining the user's context and providing a more consistent experience with other TYPO3 backend operations.

The modal displays thumbnails for supported file types, allowing users to visually verify the current file before replacement. Additionally, the following detailed information is shown:

  • File type
  • File size
  • Creation date

Impact 

This enhancement significantly improves the file management experience in TYPO3 by providing a more modern, context-aware interface for file replacement. The combination of modal-based interaction and file information display creates a more efficient and user-friendly workflow that aligns with modern web application patterns.

The changes particularly benefit content editors who frequently work with files, providing them with the visual and metadata context needed to make confident file replacement decisions without interrupting their content creation workflow.

Feature: #107436 - Symfony Translation Component integration 

See forge#107436

Description 

TYPO3 now utilizes the Symfony Translation component for reading localization label files such as XLIFF and PO, instead of its custom localization parsers.

The migration brings several improvements:

  • Standardized file parsing using Symfony's translation loaders
  • Enhanced API for accessing translation catalogues
  • Support for custom translation loaders following Symfony standards

The new system maintains backward compatibility while providing a modern foundation for future improvements with translatable labels.

In addition, all label-related configuration options have been streamlined under the $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace.

The following new configuration options have been introduced:

$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']
Configure custom translation loaders.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['format']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales']
Moved from EXTCONF.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']
Moved from SYS.locallangXMLOverride.

Custom translation loaders 

Extension developers can now implement custom translation loaders by implementing Symfony's translation loader interfaces:

Example custom loader
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;

class CustomLoader implements LoaderInterface
{
    public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
    {
        // Custom loading logic
        $catalogue = new MessageCatalogue($locale);
        // ... populate catalogue
        return $catalogue;
    }
}
Copied!

Register custom loaders via configuration:

Register custom loader in configuration
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['fileEnding']
    = \MyVendor\MyExtension\Translation\CustomLoader::class;
Copied!

Impact 

All previous configuration options have been moved to the new $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace. These are automatically migrated to the new location when accessing the install tool.

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Feature: #107441 - Allow more hashing algorithms in FAL 

See forge#107441

Description 

Previously, FAL's LocalDriver only supported md5 and sha1 as hashing algorithms. While this may be sufficient for many use cases, it might be necessary to use different hashing algorithms depending on the specific scenario.

The method LocalDriver->hash() is now able to use any hashing algorithm that is registered in PHP itself by building a HashContext object and updating it by streaming the file content.

Impact 

FAL's LocalDriver can now make use of different hashing algorithms, e.g. crc32, sha256 and many more.

Feature: #107471 - New scheduler task wizard 

See forge#107471

Description 

A wizard to create new scheduler tasks has been introduced in the Administration > Scheduler module to significantly improve the user experience when creating new scheduler tasks. The wizard replaces the previous dropdown-based task selection in FormEngine with a modern, categorized interface similar to the content element wizard.

UX improvements 

  • Categorized task selection: Tasks are now organized by extension or category for better discoverability
  • Search functionality: Users can search and filter available tasks
  • Visual task representation: Each task displays with proper icons, titles, and descriptions

Technical improvements 

  • Prevents broken records: The old system preselected the first available task type when creating new tasks, which caused validation issues when users changed the task type, since required fields for the new type might not be properly initialized
  • Clean task type preselection: The selected task type is directly passed to FormEngine, eliminating the need to change the type in the form
  • Extensible via PSR-14 event: Extensions can modify wizard items through the new ModifyNewSchedulerTaskWizardItemsEvent

PSR-14 event 

A new PSR-14 event ModifyNewSchedulerTaskWizardItemsEvent has been introduced to allow extensions to modify the wizard items.

The event provides the following methods:

  • getWizardItems(): Returns the current wizard items array
  • setWizardItems(): Sets the complete wizard items array
  • addWizardItem(): Adds a single wizard item
  • removeWizardItem(): Removes a wizard item by key
  • getRequest(): Returns the current server request

Example 

A corresponding event listener class:

Example event listener class
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Scheduler\Event\ModifyNewSchedulerTaskWizardItemsEvent;

final class ModifySchedulerTaskWizardListener
{
    #[AsEventListener('my-extension/scheduler/modify-wizard-items')]
    public function __invoke(ModifyNewSchedulerTaskWizardItemsEvent $event): void
    {
        // Add a custom task to the wizard
        $event->addWizardItem('my_custom_task', [
            'title' => 'My Custom Task',
            'description' => 'A custom task provided by my extension',
            'iconIdentifier' => 'my-custom-icon',
            'taskType' => 'MyVendor\\MyExtension\\Task\\CustomTask',
            'taskClass' => 'MyVendor\\MyExtension\\Task\\CustomTask',
        ]);

        // Remove an existing task
        $event->removeWizardItem('redirects_redirects:checkintegrity');

        // Modify existing wizard items
        $wizardItems = $event->getWizardItems();
        foreach ($wizardItems as $key => $item) {
            if (isset($item['title'])
                && str_contains($item['title'], 'referenceindex:update')) {
                $item['title'] = 'Update reference index';
                $event->addWizardItem($key, $item);
            }
        }
    }
}
Copied!

Impact 

  • The scheduler task creation workflow is significantly improved with better UX
  • The risk of creating broken task records due to task type changes is eliminated
  • Extensions can easily modify the wizard through the PSR-14 event
  • The interface is more consistent with other TYPO3 wizard interfaces
  • Task discovery is improved through categorization and search functionality

Feature: #107488 - Extensible scheduler task timing options 

See forge#107488

Description 

The scheduler task timing configuration has been migrated to the single TCA execution_details field, which is of type json. This field has now been enhanced to allow extensions to customize the corresponding timing-related fields, particularly the frequency field with custom cron expressions. This is achieved through the new overrideFieldTca option, available in the TCA field configuration.

Previously, frequency options were only configurable through the global $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions'] array, which has been moved to the TCA configuration to provide better extensibility and consistency with TYPO3's configuration patterns, see Breaking: #107488 - Scheduler frequency options moved to TCA.

Enhanced customization options 

Extensions can now override any timing-related field configuration using the overrideFieldTca mechanism in the execution_details field.

Available fields:

  • Frequency field: frequency
  • Running type field: runningType
  • Parallel execution settings: multiple
  • Start or end date fields: start and end

Example usage 

Extensions can now add custom frequency options by creating a TCA override file:

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

Extensions can also add a description:

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']
    ['overrideFieldTca']['multiple'] = [
        'description' => 'my_extension.messages:multiple.description',
    ];
Copied!

Impact 

This enhancement provides a more flexible and extensible way to configure scheduler task timing options, allowing extensions to seamlessly integrate custom timing configurations while maintaining consistency with TYPO3's configuration patterns.

Feature: #107518 - PSR-14 event to modify form elements after being added 

See forge#107518

Description 

A new PSR-14 event BeforeRenderableIsAddedToFormEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] .

The new event is dispatched immediately before a renderable has been constructed and added to the corresponding form. This allows full customization of the renderable after it has been initialized.

The event provides the $renderable public property.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

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

final class BeforeRenderableIsAddedToFormEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-added-to-form-event')]
    public function __invoke(BeforeRenderableIsAddedToFormEvent $event): void
    {
        $event->renderable->setLabel('foo');
    }
}
Copied!

Impact 

With the new BeforeRenderableIsAddedToFormEvent , it is now possible to modify a renderable after it has been initialized and right before being added to its form.

Feature: #107519 - Add "discard" command to DataHandler 

See forge#107519

Description 

The \TYPO3\CMS\Core\DataHandling\DataHandler PHP API has been extended with a new "discard" command to simplify workspace management.

This new command provides a cleaner, more explicit way to discard workspace records compared to the previous approach using version commands.

The new "discard" command can be used in the $commandArray parameter when calling the DataHandler to remove versioned records from a workspace.

Impact 

The "discard" command offers a more intuitive API for workspace operations:

  • Instead of using complex version commands with actions such as "clearWSID" or "flush", you can now use the straightforward "discard" command.
  • The command name clearly indicates its purpose.
  • The command handles all aspects of discarding workspace records, including any related child records.

Usage 

When using the discard command, it is crucial to use the UID of the versioned record (workspace version), not the UID of the live record.

Discarding a workspace record using DataHandler
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Example: Discard a versioned page record
$versionedPageUid = 123; // This must be the UID of the workspace version!

$commandArray = [
    'pages' => [
        $versionedPageUid => [
            'discard' => true,
        ],
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!
Discarding multiple records of different types
$commandArray = [
    'pages' => [
        456 => ['discard' => true], // Versioned page UID
    ],
    'tt_content' => [
        789 => ['discard' => true], // Versioned content element UID
        790 => ['discard' => true], // Another versioned content element UID
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!

Migration from legacy commands 

The new discard command replaces the previous version-based approach, which was not widely known:

Legacy approach (still supported but discouraged)
// Old way - will be removed in future versions
$commandArray = [
    'pages' => [
        $versionedUid => [
            'version' => [
                'action' => 'clearWSID',
            ],
        ],
    ],
];
Copied!
New recommended approach
// New way - cleaner and more explicit
$commandArray = [
    'pages' => [
        $versionedUid => [
            'discard' => true,
        ],
    ],
];
Copied!

The previous clearWSID and flush actions are still supported for backward compatibility but are considered deprecated and will be removed in future versions.

Feature: #107526 - Custom TCA types for scheduler tasks 

See forge#107526

Description 

Scheduler tasks can now be created using custom TCA types instead of the legacy :AdditionalFieldProvider approach. This enhancement allows developers to define custom database fields for specific task types, providing full flexibility of FormEngine and DataHandler for editing and persistence.

The new approach replaces three major downsides of the traditional method:

  1. Native database fields are now created automatically via TCA instead of being serialized into a JSON field.
  2. Tasks are registered via TCA record types instead of custom registration methods.
  3. Field handling is now done through standard TCA configuration, eliminating the need for AdditionalFieldProviders.

Benefits 

Using custom TCA types for scheduler tasks provides several advantages:

  • FormEngine handles validation automatically, reducing the risk of XSS or SQL injection vulnerabilities in extension code.
  • Task configuration follows standard TYPO3 form patterns.
  • Developers can use familiar TCA configuration instead of implementing custom field providers.
  • Access to all TCA field types, validation, and rendering options.

Migration 

Existing task types using custom TCA types automatically migrate existing data through the getTaskParameters() and setTaskParameters() methods:

  • During migration, getTaskParameters() is called to extract field values from the serialized task object.
  • For new TCA-based tasks, setTaskParameters() receives the full database record as an array instead of serialized data from the parameters field.
  • The task class name still matches the value of the tasktype field.

Implementation examples 

File storage indexing task 

TCA configuration in Configuration/TCA/Overrides/file_storage_indexing_task.php:

TCA configuration for the file storage indexing task
defined('TYPO3') or die();

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Scheduler\Task\FileStorageIndexingTask;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'scheduler.messages:fileStorageIndexing.name',
        'description' => 'scheduler.messages:fileStorageIndexing.description',
        'value' => FileStorageIndexingTask::class,
        'icon' => 'mimetypes-x-tx_scheduler_task_group',
        'iconOverlay' => 'content-clock',
        'group' => 'scheduler',
    ],
    '
        --div--;core.form.tabs:general,
            tasktype,
            task_group,
            description,
            file_storage;scheduler.messages:label.fileStorageIndexing.storage,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            --palette--;;execution,
        --div--;core.form.tabs:access,
            disable,
        --div--;core.form.tabs:extended,',
    [],
    '',
    'tx_scheduler_task'
);
Copied!

Task class with migration support:

FileStorageIndexingTask class
namespace TYPO3\CMS\Scheduler\Task;

class FileStorageIndexingTask extends AbstractTask
{
    public $storageUid = -1;

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getTaskParameters(): array
    {
        return [
            'file_storage' => $this->storageUid,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $this->storageUid = $parameters['storageUid']
            ?? $parameters['file_storage']
            ?? 0;
    }
}
Copied!

Recycler cleaner task with custom fields 

TCA configuration with custom field overrides in Configuration/TCA/Overrides/scheduler_cleaner_task.php:

TCA configuration for the recycler cleaner task
defined('TYPO3') or die();

use TYPO3\CMS\Recycler\Task\CleanerTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType(
        [
            'label' => 'recycler.tasks:cleanerTaskTitle',
            'description' => 'recycler.tasks:cleanerTaskDescription',
            'value' => CleanerTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'iconOverlay' => 'content-clock',
            'group' => 'recycler',
        ],
        '
            --div--;core.form.tabs:general,
                tasktype,
                task_group,
                description,
                selected_tables;recycler.tasks:cleanerTaskTCA,
                number_of_days;recycler.tasks:cleanerTaskPeriod,
            --div--;scheduler.messages:scheduler.form.palettes.timing,
                --palette--;;execution,
            --div--;core.form.tabs:access,
                disable,
            --div--;core.form.tabs:extended,',
        [
            'columnsOverrides' => [
                'selected_tables' => [
                    'label' => 'recycler.tasks:cleanerTaskTCA',
                    'config' => [
                        'type' => 'select',
                        'renderType' => 'selectMultipleSideBySide',
                        'size' => 10,
                        'minitems' => 1,
                        'maxitems' => 100,
                        'itemsProcFunc' =>
                            CleanerTask::class. '->getAllTcaTables',
                        'items' => [],
                    ],
                ],
            ],
        ],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Task class with migration support:

CleanerTask class
namespace TYPO3\CMS\Recycler\Task;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class CleanerTask extends AbstractTask
{
    protected int $period = 0;

    protected array $tcaTables = [];

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getAdditionalInformation()
    {
        $message = sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionTables'
            ),
            implode(', ', $this->tcaTables)
        );
        $message .= '; ';
        $message .= sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionDays'
            ),
            $this->period
        );
        return $message;
    }

    public function getTaskParameters(): array
    {
        return [
            'selected_tables' => implode(',', $this->tcaTables),
            'number_of_days' => $this->period,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $tcaTables = $parameters['RecyclerCleanerTCA']
            ?? $parameters['selected_tables']
            ?? [];
        if (is_string($tcaTables)) {
            $tcaTables = GeneralUtility::trimExplode(',', $tcaTables, true);
        }
        $this->tcaTables = $tcaTables;
        $this->period = (int)(
            $parameters['RecyclerCleanerPeriod']
            ?? $parameters['number_of_days']
            ?? 180
        );
    }
}
Copied!

Creating custom task types 

To create a custom scheduler task with TCA configuration:

  1. Create the task class extending AbstractTask. Implement the core logic in the execute() method.
  2. Implement migration methods getTaskParameters() and setTaskParameters().
  3. Optionally add getAdditionalInformation() for backend listing output.
  4. Add TCA configuration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php.
  5. Define custom fields in the TCA showitem configuration.
  6. Register the task type using ExtensionManagementUtility::addRecordType().

Example custom task TCA registration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php:

Example custom task TCA registration
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTCAcolumns(
    'tx_scheduler_task',
    [
        'custom_field' => [
            'label' => 'my_extension.tasks:custom_field',
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'My Custom Task',
        'description' => 'Description of what this task does',
        'value' => \MyVendor\MyExtension\Task\CustomTask::class,
        'icon' => 'my-custom-icon',
        'iconOverlay' => 'my-custom-icon-overlay',
        'group' => 'my_extension',
    ],
    '
        --div--;General,
            tasktype,
            task_group,
            description,
            custom_field,
            number_of_days;my_extension.tasks:myTaskPeriodLabel,
        --div--;Timing,
            --palette--;;execution,
        --div--;Access,
            disable,
    ',
    [
        'columnsOverrides' => [
            'number_of_days' => [
                'config' => [
                    'eval' => 'required',
                ],
            ],
        ],
    ],
    '',
    'tx_scheduler_task'
);
Copied!

Impact 

  • Extension developers can create more maintainable and secure scheduler tasks.
  • Custom task configuration benefits from full FormEngine capabilities.
  • Existing tasks are automatically migrated without data loss.
  • Development follows standard TYPO3 TCA patterns.
  • Improved validation and security through FormEngine and DataHandler.

Feature: #107528 - PSR-14 event before renderable is removed from form 

See forge#107528

Description 

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

The new event is dispatched immediately before a renderable is deleted from the form.

The event provides the following public properties:

  • $renderable: The form element (read-only).
  • $preventRemoval: A boolean flag that can be set to true to prevent the removal of the renderable.

The event is stoppable. As soon as $preventRemoval is set to true, no further listeners are executed.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent;

class MyEventListener {

    #[AsEventListener(
        identifier: 'my-extension/before-renderable-is-removed-from-form-event',
    )]
    public function __invoke(BeforeRenderableIsRemovedFromFormEvent $event): void
    {
        $event->preventRemoval = true;
        $renderable = $event->renderable;
        // Custom logic before the renderable is removed
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRemovedFromFormEvent , it is now possible to prevent the deletion of a renderable and to add custom logic based on the deletion.

Feature: #107537 - System resource API for system file access and public URI generation 

See forge#107537

Description 

TYPO3 allows files to be configured for multiple purposes. For example, a logo to be shown on the login page, CSS and JavaScript files to be added to web pages, or icons to be used for records. Most of the time, these are specified using the EXT syntax to reference a file within an extension. However, it can sometimes also be useful to reference an external URL or a file from a FAL storage.

To achieve this, TYPO3 Core code previously needed to parse the specified configuration, identify the type (extension file, FAL, URL), and then generate a URL from it. While there was some API available for this purpose, it was incomplete and consisted of multiple parts that were not clearly named, making them easy to misuse. As a consequence, code in the TYPO3 Core differed depending on where the configuration was used.

For users, this could lead to issues when a specific resource syntax worked in one place (for example, in TypoScript) but not in another (for example, in a Fluid email template). Moreover, the generated URLs could be inconsistent — sometimes including a cache-busting addition and sometimes not.

Third-party developers struggled with a scattered API that was hard to use and understand, especially regarding which methods should be called, in what order, and how to maintain compatibility between Composer mode and classic mode.

The new System Resource API addresses these shortcomings and enables many additional features to be built on top of it.

The code for resource resolving and URL generation is now encapsulated in a precise and centralized API in one place. This not only makes it easier to maintain and fix bugs, but ensures that such fixes apply automatically to all parts of the system where resources are configured.

Before diving into the details of the API, some terminology is clarified, followed by a top-level overview.

Naming conventions and top-level overview 

A system resource is a file or folder within a TYPO3 project. This can be a:

  • package resource – a file within an extension
  • FAL resource – a file from a FAL storage
  • app resource – a file in the TYPO3 project folder
  • URI resource – a URL

Package resource 

A package resource can now be specified with a new syntax like this:

PKG:my-vendor/package-name:Resources/Public/Icons/Extension.svg

It consists of three parts, separated by a colon (:):

  1. PKG prefix
  2. Composer name of the package (also possible in classic mode for extensions that contain a composer.json)
  3. Relative path to the file within the package

The well-known EXT syntax can still be used for the time being:

EXT:ext_name/Resources/Public/Icons/Extension.svg

This syntax is not yet deprecated, but users are advised to switch to the new syntax for new projects.

App resource 

An app resource is a file or folder within your TYPO3 installation. Such files can also be specified using the PKG syntax, using the virtual name typo3/app as package name:

PKG:typo3/app:public/typo3temp/assets/style.css

By default, only access to a fixed set of folders is allowed: public/_assets, public/typo3temp/assets, and public/uploads. Additional allowed folders can be configured via:

$GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths']

FAL resource 

While users are encouraged to use package and app resources, it is also possible to specify files from FAL storages using the following syntax:

FAL:1:/identifier/of/file.svg
Copied!

It consists of three parts, separated by a colon (:):

  1. FAL prefix
  2. Storage ID (UID of the FAL storage record)
  3. Identifier of the file within this storage (for hierarchical storages, a slash-separated path)

URI resource 

A fully qualified URL (including HTTP(S) scheme) can be specified:

https://www.example.com/my/image.svg

URIs relative to the current host can be specified by prefixing them with URI: like so:

URI:/path/to/my/image.svg

The string after the URI: prefix must be a valid URI. This means, that TYPO3 will now throw an exception, rather than rendering an invalid URI to HTML, when an invalid URI is provided.

Legacy resource annotations 

The following legacy string representations can still be used, but they are deprecated and will be removed in future TYPO3 versions:

  • FAL resource (relative path to the default FAL storage): fileadmin/identifier/of/file.svg
  • App resource (relative path to the project’s public directory): typo3temp/assets/style.css
  • App resource (relative path to the project’s public directory): _assets/vite/foo.css

All representations mentioned here are resource identifiers. They are strings that uniquely identify a resource within the system.

API description 

The PHP API consists of two parts:

  1. Resource resolving
  2. URI generation

The result of resource resolving is an object (different objects for different resource types), which can then be passed to the API for URI generation.

Example PHP usage 

use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(string $resourceIdentifier, ServerRequestInterface $request): string
{
    $resource = $this->systemResourceFactory->createPublicResource($resourceIdentifier);
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

The SystemResourceFactory and an implementation of a system resource publisher are injected via dependency injection (DI). The resource publisher is referenced using the interface SystemResourcePublisherInterface .

In the renderUrl() method, the resourceFactory is used to obtain a resource object from a given resource identifier using createPublicResource(). This object is then passed to the generateUri() method of the resourcePublisher.

The method has a second required argument for the current request. If the code has no access to the current request, null can be passed instead. Passing null is discouraged and might be deprecated in future TYPO3 versions. For CLI commands, passing null is fine, but absolute URIs (including scheme and hostname) cannot be generated in this case.

Example Fluid usage 

A new <f:resource> ViewHelper converts a resource identifier into an object that can be passed to other ViewHelpers. Currently only <f:uri.resource> accepts such resource objects, but more (for example <f:image>) will support them in the future.

<html>
<head>
    <link href="{f:resource(identifier:'PKG:typo3/cms-backend:Resources/Public/Css/webfonts.css') -> f:uri.resource()}" rel="stylesheet" />
</head>
<body>
    <f:asset.css identifier="main.css" href="PKG:my-vendor/site-package:Resources/Public/Css/main.css"/>
    <img src="{typo3.systemConfiguration.backend.loginLogo -> f:resource() -> f:uri.resource()}" alt="Logo" height="41" width="150" />
</body>
Copied!

All resource identifiers mentioned above can be passed to existing ViewHelpers like <f:asset>.

Example TypoScript usage 

page.includeCSS.extensionResource = EXT:backend/Resources/Public/Css/webfonts.css
page.includeCSS.packageResource = PKG:my-vendor/ext-name:Resources/Public/Css/main.css
page.includeCSS.appResource = PKG:typo3/app:public/_assets/style.css
page.includeCSS.uriResource = https://www.example.com/css/main.css
page.includeCSSLibs.fal = FAL:1:/templates/css/main.css
Copied!

CSS and JavaScript files can be referenced using all resource identifiers mentioned above.

Impact 

All parts of the TYPO3 Core where URLs are generated from specified resources now use the new API. This means all of those places support the new PKG syntax and have cache busting applied automatically.

TYPO3 installations using other syntax than the supported ones need to migrate their resource references. Resource identifiers such as typo3conf/ext/my_ext/Resources/Public/Image.svg no longer work consistently throughout the system and must be replaced with EXT or PKG resource identifiers.

The benefits are:

  • Consistency throughout the system for system resource resolving and URL generation.
  • A more intuitive API that helps avoid mistakes, including security-related ones.
  • Consolidation of all code currently resolving system resources and generating URLs.
  • A foundation for future features such as publishing system resources to a CDN and letting TYPO3 generate CDN URLs directly instead of parsing HTML output.

Outlook 

This change is a major step forward, but further improvements are planned:

  • Implement flexible resource publishing in Composer and classic mode, allowing different publishing strategies and configuration of additional public resource folders or files.
  • Use this API in all areas that consume private resources, most notably Fluid template files.
  • Replace FAL storage for system assets completely by leveraging the app resource to contain templates, CSS files, logos, images, or even complete themes.

Feature: #107566 - PSR-14 event after current page is resolved 

See forge#107566

Description 

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

The new event is dispatched after the current page has been resolved.

The event provides the following public properties:

  • $currentPage: The current page.
  • $formRuntime: The form runtime object (read-only).
  • $lastDisplayedPage: The last displayed page (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent;

final class AfterCurrentPageIsResolvedEventListener
{
    #[AsEventListener('my-extension/after-current-page-is-resolved-event')]
    public function __invoke(AfterCurrentPageIsResolvedEvent $event): void
    {
        $event->currentPage->setRenderingOption('enabled', false);
    }
}
Copied!

Impact 

With the new AfterCurrentPageIsResolvedEvent , it is now possible to manipulate the current page after it has been resolved.

Feature: #107568 - PSR-14 event before renderable is validated 

See forge#107568

Description 

A new PSR-14 event BeforeRenderableIsValidatedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] .

The new event is dispatched right before a renderable is validated.

The event provides the following public properties:

  • $value: The submitted value of the renderable.
  • $formRuntime: The form runtime object (read-only).
  • $renderable: The renderable (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Error\Error;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Localization\TranslationService;
use TYPO3\CMS\Form\Event\BeforeRenderableIsValidatedEvent;

final class BeforeRenderableIsValidatedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-validated')]
    public function __invoke(BeforeRenderableIsValidatedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'AdvancedPassword') {
            return;
        }

        $elementValue = $event->value;
        if ($elementValue['password'] !== $elementValue['confirmation']) {
            $processingRule = $renderable
                ->getRootForm()
                ->getProcessingRule($renderable->getIdentifier());

            $processingRule->getProcessingMessages()->addError(
                GeneralUtility::makeInstance(
                    Error::class,
                    GeneralUtility::makeInstance(TranslationService::class)->translate('validation.error.1556283177', null, 'EXT:form/Resources/Private/Language/locallang.xlf'),
                    1556283177
                )
            );
        }

        $event->value = $elementValue['password'];
    }
}
Copied!

Impact 

With the new BeforeRenderableIsValidatedEvent , it is now possible to modify or validate the value of a renderable element before TYPO3 Core performs its built-in validation logic. This allows extensions to inject custom validation rules or preprocessing steps before standard validation runs.

Feature: #107569 - PSR-14 event before renderable is rendered 

See forge#107569

Description 

A new PSR-14 event BeforeRenderableIsRenderedEvent has been introduced. It serves as a replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] .

The new event is dispatched right before a renderable is rendered.

The event provides the following public properties:

  • $renderable: The form element.
  • $formRuntime: The form runtime.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent;

final class BeforeRenderableIsRenderedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-rendered')]
    public function __invoke(BeforeRenderableIsRenderedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'Date') {
            return;
        }

        $date = $event->formRuntime[$renderable->getIdentifier()];
        if ($date instanceof \DateTime) {
            $event->formRuntime[$renderable->getIdentifier()] =
                $date->format('Y-m-d');
        }
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRenderedEvent , it is now possible to modify a renderable before it is rendered or adjust the form runtime data before rendering.

Feature: #107581 - Improve handling of FinisherException 

See forge#107581

Description 

\FinisherException instances thrown during form processing are now caught within their respective finisher. Instead of resulting in a generic 503 error page, the exception is logged, and a user-friendly error message is displayed to the user, indicating that the form could not be submitted successfully.

The error message can be customized for each finisher using the new errorMessage option. Additionally, the newly introduced Error template can be overridden and customized.

Impact 

Users no longer see a 503 error page if a FinisherException occurs. Instead, they receive a clear, user-friendly message in the form frontend. All FinisherExceptions are logged for further analysis and debugging.

This change improves the user experience and makes error handling in forms more flexible and transparent.

Feature: #107628 - Improved backend module naming and structure 

See forge#107628

Description 

TYPO3 Core's backend module structure has been modernized with clearer, more intuitive naming conventions that better align with industry standards used by enterprise content management systems.

Module names should immediately convey their purpose to editors and administrators. This series of renamings improves clarity, discoverability, and reduces the cognitive load for both new and experienced users.

Clear and consistent naming is fundamental to good user experience. When comparing TYPO3 Core with other enterprise CMS platforms, it became evident that some of TYPO3 Core's module names were either too technical, ambiguous, or did not clearly communicate their purpose.

To summarize, the goals of this restructuring are:

  • Improved clarity: Module names immediately convey their purpose.
  • Industry alignment: Adopt naming conventions familiar to users of other enterprise CMS platforms.
  • Better discoverability: Help new users find functionality without extensive training.
  • Reduced cognitive load: Minimize confusion when navigating the backend.

Module renamings 

The following modules have been renamed.

Top-level modules 

Web => Content 

The top-level Web module has been renamed to Content.

Rationale: TYPO3 is a content management system. The primary workspace where editors create and manage content should be clearly labeled as such. The term "Web" was ambiguous and did not communicate the module's purpose. Users outside the TYPO3 ecosystem often did not understand what "Web" meant in this context.

Migration: Update module parent references from 'web' to 'content':

return [
    'my_module' => [
        'parent' => 'content',  // Previously: 'web'
    ],
];
Copied!

File => Media 

The top-level File module has been renamed to Media.

Rationale: The term "Media" clearly indicates the module's purpose of managing digital media files (images, videos, documents, audio files, etc.) within the CMS. The term "File" was too generic and technical, while "Media" is widely understood and commonly used in content management systems to refer to digital content assets.

Migration: Update module parent references from 'file' to 'media':

return [
    'my_module' => [
        'parent' => 'media',  // Previously: 'file'
    ],
];
Copied!

Site Management => Sites 

The top-level Site Management module has been renamed to Sites.

Rationale: The former label "Site Management" was long and formal, and did not align with TYPO3’s evolving, concise module naming strategy. The simplified name "Sites" improves scanability in the module menu and matches the naming of other top-level modules. It also better reflects the purpose of the module: providing an overview entry point for all configured (web)sites.

Migration: The top-level module identifier site is kept. No migration is necessary.

Admin (tools) <=> System 

The top-level module formerly known as Admin tools is now called Administration.

The purpose of this top-level module has changed. It now contains those modules useful to backend administrators, such as user and permission management, the Scheduler, and Integrations.

Most modules formerly found in Admin tools are now located in System.

Rationale: The top-level module Administration now contains modules that are used by backend administrators in their daily work. Modules that require system maintainer permissions are found in the module named System.

Migration: Modules generally accessible to backend administrators should be moved to the top-level module with the identifier admin. Modules that require system maintainer permissions or are mainly useful to system maintainers and DevOps should be moved to system.

return [
    'my_administration_module' => [
        'parent' => 'admin',  // Previously: 'system'
        'access' => 'admin',
    ],
    'my_maintainer_module' => [
        'parent' => 'system',  // Previously: 'tools'
        'access' => 'systemMaintainer',
    ],
];
Copied!

Second-level modules 

For modules, where the module identifier changed, the upgrade wizard "Migrate module permissions" migrates module level group and user permissions.

Page => Layout 

The second-level Page module has been renamed to Layout to better match its scope.

Rationale: The previous module name "Page" did not clearly convey the module’s purpose or workflow. TYPO3 provides multiple ways to interact with a page (e.g. structure, properties, preview), and the term "Page" alone did not describe which aspect was being managed. The renamed module "Layout" more accurately reflects what editors do inside the module: maintain the page layout, manage content elements, and organize them into the correct columns and grids. This provides clearer expectations, improves usability for new editors, and aligns the module name with modern TYPO3 workflows and terminology.

Migration: Since the module is just renamed, there are no migrations necessary.

List => Records 

The second-level List module has been renamed to Records to better convey its purpose and improve clarity.

Rationale: The term "List" is too generic and does not adequately communicate the module's purpose. While "List" could refer to any kind of enumeration or overview, the module actually provides structured access to database records appearing on a page. The new name "Records" is more specific and immediately communicates that this module is about working with data records—viewing, editing, and managing them at the database level.

The term "Records" also aligns with how the module is already described in its own interface ("List of database records") and better reflects the technical nature of the module's functionality. For users coming from other enterprise CMS platforms or database-driven systems, "Records" is a widely understood term that clearly indicates low-level data management capabilities.

This renaming reduces ambiguity and helps users—especially those new to TYPO3—understand that this module provides direct access to the underlying record structure, distinguishing it from the more content-focused Layout module.

Migration: The module identifier has been renamed from web_list to records. An alias is in place. However, use the new identifier when referencing.

- $this->uriBuilder->buildUriFromRoute('web_list');
+ $this->uriBuilder->buildUriFromRoute('records');
Copied!

View => Preview 

The second-level View module has been renamed to Preview to better match its scope. It has also been moved one position down after Records, as that module is considered more important for daily work.

Rationale: The term "Preview" is more precise, as it triggers a frontend preview and cannot be misunderstood as "viewing" a page in the backend context.

Migration: Since the module is already internally referred to as page_preview, no changes in referencing modules are required.

Workspaces => Publish 

The second-level Workspaces module has been renamed to Publish to better match its current scope.

Rationale: The initially introduced "Workspaces administration" tool has been reworked to move content through a publishing process in past versions. For this reason, it is now renamed to "Publish" and is only visible when inside a workspace.

Migration: The module has internally been renamed to workspaces_publish. A module alias is in place, so references to the old workspaces_admin identifier keep working as before, but it is recommended to adapt usages.

The upgrade wizard "Migrate module permissions" migrates backend user and group-level permissions for this module.

Settings => Setup 

The second level Settings module has been integrated into Setup.

Rationale: Combining the "Setup" and "Settings" gives a more concise view, since managing sites and site settings are often done as one task.

Migration: The module identifier site_settings has been removed, the existing actions edit, save and dump have been renamed to editSettings, saveSettings and dumpSettings as part of the site_configuration module identifier.

System > Backend Users => Administration > Users 

The second-level System > Backend Users module has been renamed to Administration > Users to better match its scope.

It has also been moved to the top of the to Administration top level menu as it is frequently used by administrators.

Rationale: The new name "Users" is shorter and easier to recognize in the module menu. While "Backend Users" was technically precise, the simpler term improves readability and usability, making the module easier to find for administrators performing common user management tasks.

Migration: The identifier backend_user_management is kept unchanged, no migration needed.

System > DB Check => System > Database 

The second-level DB Check module has been renamed to Database to better reflect its purpose.

Rationale: The name "Database" is clearer and avoids the ambiguity of the abbreviation "DB". The module now focuses solely on search and query functionality and no longer performs database integrity checks.

Migration: The module identifier has changed from system_dbint to system_database. An alias ensures backward compatibility. Use the new identifier when registering custom modules.

 return [
     'my_database_tool' => [
-        'parent' => 'system_dbint',
+        'parent' => 'system_database',
     ],
 ];
Copied!

Impact 

All renamed module identifiers maintain their previous names as aliases, ensuring full backward compatibility. Existing code, configurations, and third-party extensions continue to work without modification.

Developers are encouraged to update their code to use the new identifiers for consistency and clarity.

The modernized naming improves the overall user experience by making the backend more intuitive and easier to navigate, particularly for users familiar with other enterprise CMS platforms.

Feature: #107663 - Introduce submodule dependencies 

See forge#107663

Description 

Backend modules can now declare a dependency on their submodules using the new appearance['dependsOnSubmodules'] configuration option. When enabled, a module will automatically hide itself from the module menu if none of its submodules are available to the current user.

This feature enables container modules to adapt dynamically to the user's permissions and installed extensions, ensuring the module menu remains clean and only displays modules that provide actual functionality.

Example configuration 

EXT:my_extension/Configuration/Backend/Modules.php
use TYPO3\CMS\Info\Controller\InfoModuleController;

return [
    'content_status' => [
        'parent' => 'content',
        'access' => 'user',
        'path' => '/module/content/status',
        'iconIdentifier' => 'module-info',
        'labels' => 'backend.modules.status',
        'aliases' => ['web_info'],
        'navigationComponent' => '@typo3/backend/tree/page-tree-element',
        'appearance' => [
            'dependsOnSubmodules' => true,
        ],
        'showSubmoduleOverview' => true,
    ],
    'web_info_overview' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
    'web_info_translations' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
];
Copied!

In this example, the Content > Status module will only be shown in the module menu if at least one of its submodules (Overview, Translations) is available to the current user.

If all submodules are either disabled, removed, or the user lacks access permissions to them, the parent module will automatically be hidden from the module menu.

Impact 

Module menus become more intuitive and user-focused. Container modules equipped with appearance['dependsOnSubmodules'] intelligently adapt to the current context, appearing only when they offer actionable functionality to the user.

The Content > Status module leverages this feature to seamlessly disappear from the module menu when extensions are uninstalled or when users lack permissions to access its submodules, preventing dead-end navigation paths and enhancing the overall backend experience.

Feature: #107668 - Improve scheduler task group handling and display 

See forge#107668

Description 

The scheduler module has been enhanced with improved visual organization and quick editing capabilities for task groups. Task groups can now be assigned custom colors to improve visual distinction, and the group name has been made directly editable from the module view.

A new color field has been added to the tx_scheduler_task_group table, allowing administrators to assign hex color values to each task group.

UI improvements 

The scheduler module now provides several enhancements for task groups:

Color coding
Task groups can be assigned a color that is displayed as a left border on the group panel, similar to the page module label. This makes it easy to visually distinguish between different groups at a glance.
Quick edit link
The group name is now a clickable link that opens the group record for editing, providing direct access to the group name, color, and description fields without navigating through the list module.
Description display
Group descriptions are now displayed in the scheduler module directly below the group name, providing additional context about the purpose of each group.
Bold group names
Group names are now displayed in bold for better visual hierarchy and readability in the scheduler module interface.

Implementation details 

The color field is implemented as a TCA field of type 'color' with the following characteristics:

  • Stores standard hex color values (for example #FF8700).
  • Includes a value picker with 11 predefined colors aligned with the TYPO3 brand palette.

Predefined color options:

  • TYPO3 Orange (#FF8700)
  • White (#ffffff)
  • Gray (#808080)
  • Black (#000000)
  • Blue (#2671d9)
  • Purple (#5e4db2)
  • Teal (#2da8d2)
  • Green (#3cc38c)
  • Magenta (#c6398f)
  • Yellow (#ffbf00)
  • Red (#d13a2e)

These options can be customized by manipulating the $GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'] array in your TCA overrides file.

Add a custom color option in a TCA override file
$GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'][] = [
    'label' => 'My Color',
    'value' => '#ABCDEF',
];
Copied!

Impact 

These enhancements improve the usability of the scheduler module, particularly for installations with many task groups. The improvements allow administrators to:

  • Quickly identify related task groups through visual color coding.
  • Edit group properties directly from the scheduler module.
  • Organize tasks by category, priority, or purpose.
  • See group descriptions without opening the group record.
  • Improve visual navigation in large scheduler configurations.

The feature is fully backward compatible. Existing task groups without colors continue to work as before, displaying without a colored border.

Feature: #107679 - PSR-14 event for custom record retrieval in LinkBuilder 

See forge#107679

Description 

A new PSR-14 event \TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent has been introduced to retrieve a record using custom code in the \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

The event is dispatched with $record set to null. If an event listener retrieves a record from the database, it can assign the record as an array to $record. Doing so stops event propagation and skips the default record retrieval logic in \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

Custom event listeners must handle all aspects normally performed by DatabaseRecordLinkBuilder, such as record visibility, language overlay, or version overlay, if relevant.

The event provides the following public properties (all read-only, except for $record):

  • $linkDetails: Information about the link being processed.
  • $databaseTable: The name of the database table the record belongs to.
  • $typoscriptConfiguration: The full TypoScript link handler configuration.
  • $tsConfig: The full TSconfig link handler configuration.
  • $request: The current request object.
  • $record: The database record as an array (initially null).

Example 

An example event listener could look like:

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

final class MyEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/before-database-record-link-resolved',
    )]
    public function __invoke(BeforeDatabaseRecordLinkResolvedEvent $event): void
    {
        // Retrieve the record from the database as an array
        $result = /* ... */;

        if ($result !== false) {
            // Setting the record stops event propagation and
            // skips the default record retrieval logic
            $event->record = $result;
        }
    }
}
Copied!

Impact 

This new event allows developers to implement custom record retrieval logic for links created with typolink, for example to apply custom access restrictions or fetch data from alternative sources before rendering a link.

Feature: #107683 - File storage tree items modification event and label support 

See forge#107683

Description 

Similar to the page tree functionality introduced in TYPO3 v12 and v13, the file storage tree now supports modification of tree items through the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent .

The event is dispatched in the file storage TreeController after the storage tree items have been resolved and prepared. It provides the current PSR-7 request as well as the collection of file storage tree items.

Additionally, labels can now be added to file storage tree nodes via user TSconfig, using the combined identifier of the folder:

EXT:my_extension/Configuration/user.tsconfig
options.folderTree.label.1:/campaigns/ {
    label = Main Storage
    color = #ff8700
}
Copied!

Labels and status information 

Similar to the page tree, labels and status information can be added to file storage tree nodes. These features significantly improve the clarity and accessibility of the file storage tree component:

  • Labels: Each node can support multiple labels, sorted by priority. The highest priority label takes precedence, and only its marker is rendered. All additional labels are added to the title attribute of the node.
  • Status information: Can be added through the event to provide additional visual feedback. Like labels, status information is sorted by priority. Only the highest priority status indicator is displayed, while all status labels are added to the title attribute.

Example event listener 

EXT:my_extension/Classes/Backend/EventListener/ModifyFileStorageTreeItems.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-file-storage-tree-items',
)]
final readonly class ModifyFileStorageTreeItems
{
    public function __invoke(AfterFileStorageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();

        foreach ($items as &$item) {
            // Add special label for storage with uid 1
            if ($item['resource']->getCombinedIdentifier() === '1:/campaigns/') {
                $item['labels'][] = new Label(
                    label: 'A label',
                    color: '#abcdef',
                    priority: 10,
                );
                $item['statusInformation'][] = new StatusInformation(
                    label: 'An important information',
                    severity: ContextualFeedbackSeverity::INFO,
                    priority: 10,
                    icon: 'content-info',
                );
            }
        }

        $event->setItems($items);
    }
}
Copied!

Impact 

It is now possible to modify the prepared file storage tree items before they are returned by the TreeController , using the new PSR-14 event AfterFileStorageTreeItemsPreparedEvent . Additionally, labels can be assigned to file storage tree nodes via user TSconfig.

Using these functionalities helps provide visual cues and improved accessibility for editors working with file storages and folders.

Feature: #107697 - Unified file creation in Element Browser 

See forge#107697

Description 

The file creation functionality in the TYPO3 backend has been significantly improved and unified to provide a consistent user experience. Previously, creating new files required navigating to a separate page, which resulted in context loss and an inconsistent interface compared to folder creation.

File creation is now fully integrated into the Element Browser pattern, similar to how folder creation works. This change brings a consistent, modern, and more intuitive workflow to file management.

Key improvements 

Unified interface
File creation now uses the same modal-based Element Browser interface as folder creation, providing a consistent look and feel across all file operations.
Single entry point
The new New File button provides access to all file creation methods: file upload with drag-and-drop, online media (YouTube, Vimeo), and text file creation — all in one unified interface.
Context preservation
The modal interface keeps users in context with the file storage tree visible, eliminating the need to navigate away from the current view.
Advanced upload handling
The new implementation uses the drag-uploader component, which provides sophisticated duplication handling (replace, rename, skip, or use existing), replacing the previous simple "override existing" checkbox.
File extension filtering
File creation forms are automatically filtered based on allowed file types and hidden when no valid file extensions are available for the current context.
Updated button labels
For consistency and clarity, button labels have been updated from Create Folder / Create File to New Folder / New File, better reflecting the nature of the actions.

How to use 

In the File List module, click the New File button in the document header toolbar to open a modal dialog. This modal provides three ways to add files:

  1. Upload files - Click Select & upload files to choose files. The drag-and-drop uploader supports advanced duplication handling, allowing you to choose whether to replace, rename, skip, or use existing files when conflicts occur.
  2. Add online media - Enter a URL from supported online media platforms (YouTube, Vimeo) to add media files directly from the web.
  3. Create text file - Create a new empty text file with supported extensions (based on system configuration).

The modal keeps the file storage tree visible, allowing users to navigate between folders without losing context. All three options are available in a single, unified interface, streamlining the file creation workflow.

The previous Upload file button has been removed.

Impact 

The unified file creation interface provides a significantly improved user experience through better context preservation, advanced duplication handling, and a consistent modal-based workflow.

Editors benefit from having all file creation options in one place, eliminating the need to navigate between different pages for different file operations. The modal-based approach keeps the file storage tree visible, making it easier to work with files across multiple folders within a single, continuous workflow.

Feature: #107710 - Support for XLIFF 2.x translation files 

See forge#107710

Description 

TYPO3 now supports both XLIFF 1.2 and XLIFF 2.x translation file formats. The XLIFF loader automatically detects which version is used and parses the file accordingly, making the transition seamless for integrators and extension authors.

XLIFF (XML Localization Interchange File Format) is an XML-based format for storing translatable content. While TYPO3 has traditionally used XLIFF 1.2, the XLIFF 2.x standard brings improvements in structure and simplification.

Version detection 

The XLIFF loader automatically detects the file version by examining:

  1. The XML namespace (urn:oasis:names:tc:xliff:document:2.0 for XLIFF 2.0)
  2. The version attribute in the root element

No configuration or manual intervention is required - both formats work transparently side by side.

Key differences between XLIFF 1.2 and XLIFF 2.x 

For integrators working with translation files, here are the main structural differences:

XLIFF 1.2 structure:

  • Uses <trans-unit> elements directly within <body>
  • Translation approval via approved attribute (yes/no)
  • Namespace: urn:oasis:names:tc:xliff:document:1.2

Example XLIFF 1.2:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="de">
        <body>
            <trans-unit id="button.submit" approved="yes">
                <source>Submit</source>
                <target>Absenden</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

XLIFF 2.0 structure:

  • Uses <unit> elements containing <segment> elements
  • Translation state via state attribute on <segment> (initial, translated, reviewed, final)
  • More granular and modern structure
  • Namespace: urn:oasis:names:tc:xliff:document:2.0

Example XLIFF 2.0:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0"
       srcLang="en" trgLang="de">
    <file id="f1">
        <unit id="button.submit">
            <segment state="final">
                <source>Submit</source>
                <target>Absenden</target>
            </segment>
        </unit>
    </file>
</xliff>
Copied!

Translation approval handling 

TYPO3's requireApprovedLocalizations configuration is respected for both formats:

  • XLIFF 1.2: Translations with approved="no" are skipped when approval is required
  • XLIFF 2.x: Translations with state="initial" or state="translated" are treated as not approved, while state="final" and state="reviewed" are considered approved

Impact 

Extension authors and integrators can now use either XLIFF 1.2 or XLIFF 2.x format for their translation files. Existing XLIFF 1.2 files continue to work without any changes, while new projects can leverage the more modern XLIFF 2.x standard.

This also improves compatibility with modern translation tools and services that have adopted the XLIFF 2.0 standard.

Feature: #107712 - Introduce card-based submodule overview 

See forge#107712

Description 

Backend modules can now display a card-based overview of their submodules instead of automatically redirecting to the first available submodule. This new showSubmoduleOverview configuration option enables a more user-friendly navigation experience, similar to the Install Tool's maintenance card layout.

When enabled, clicking on a second-level module displays an overview page with cards for each accessible submodule. Each card shows the module's icon, title, description, and an "Open module" button, allowing users to make an informed choice about which submodule to access.

Example configuration 

return [
    'web_info' => [
        'parent' => 'content',
        'showSubmoduleOverview' => true,
        // ...
    ],
    'web_info_overview' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/overview',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.overview',
        // ... routes and other configuration
    ],
    'web_info_translations' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/translations',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.translations',
        // ... routes and other configuration
    ],
];
Copied!

In this example, the Content > Status module displays a card-based overview showing both Pagetree Overview and Localization Overview submodules. Users can read the description of each module before deciding which one to open.

The feature works seamlessly with the existing module permission system. Only submodules that the current user has access to are displayed in the overview. If no accessible submodules exist, a helpful information message is shown instead of an empty page.

Implementation details 

The showSubmoduleOverview option modifies the behavior in several key areas:

  1. Module routing - When set to true, the module's default route targets SubmoduleOverviewController instead of automatically redirecting to the first available submodule.
  2. Middleware behavior - The BackendModuleValidator middleware skips its automatic submodule redirection logic when this option is enabled, allowing the overview page to be displayed.
  3. Navigation enhancement - The system provides automatic navigation capabilities:

    • The SubmoduleOverviewController displays the submodule jump menu, allowing quick access to all available submodules.
    • When showSubmoduleOverview is activated, the ModuleTemplate automatically adds a "Module Overview" menu item to the submodule dropdown.
    • This allows users to easily navigate back to the overview from any submodule, especially useful when a submodule does not manually provide a "go back" button.

To provide meaningful descriptions on the overview cards, modules should define a description or shortDescription in their labels configuration. These are displayed in the card body to help users understand each submodule's purpose.

Impact 

Backend navigation becomes more intuitive and self-documenting. Container modules using showSubmoduleOverview provide users with a clear overview of available functionality, eliminating the confusion of being automatically redirected to an arbitrary first submodule.

The Content > Status module now uses this feature to present its submodules in an accessible, visually organized manner. Users can quickly understand what each submodule offers before navigating to it, improving discoverability and user experience in the TYPO3 backend.

Feature: #107725 - Support username for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password. With this change, the Redis cache backend in TYPO3 now supports both authentication mechanisms.

You can configure the Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend'] = RedisBackend::class;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The RedisBackend now supports authentication using both a username and a password.

The password configuration option is now typed as array|string. Using an array for this configuration option is deprecated and will be removed in TYPO3 v15.0.

Feature: #107755 - Add creation information for redirects 

See forge#107755

Description 

The redirects component has been extended with additional meta information to improve audit trails and team collaboration. Redirects now automatically capture and display:

  • Creation timestamp
  • Creating backend user

After months or even years, teams frequently wonder why a particular redirect exists and who created it. Displaying the creator (backend user) and creation date directly in the backend module makes auditing and communication significantly easier.

Impact 

All newly created redirects automatically include creation information, regardless of whether they are created:

  • Manually through the redirects backend module
  • Automatically when updating a page slug
  • Programmatically through the DataHandler API

Existing redirects created before this feature will only show the creation date, as the user information was not tracked previously.

Feature: #107759 - TranslateViewHelper supports translation domain syntax 

See forge#107759

Description 

The Fluid <f:translate> ViewHelper now supports the new translation domain syntax, providing a more concise and readable way to reference translation labels.

A new domain attribute has been added that accepts both traditional extension names and the new translation domain names.

The ViewHelper now supports multiple ways to specify translations:

  1. New domain attribute - recommended for new code:

    <f:translate key="form.legend" domain="my_extension" />
    <f:translate key="form.legend" domain="my_extension.messages" />
    Copied!
  2. Inline domain syntax in key - shortest form:

    <f:translate key="my_extension.messages:form.legend" />
    <f:translate key="LLL:my_extension.messages:form.legend" />
    Copied!
  3. Traditional extensionName attribute - still supported:

    <f:translate key="form.legend" extensionName="MyExtension" />
    <f:translate key="form.legend" extensionName="my_extension" />
    Copied!
  4. Full LLL reference - classic syntax, still supported:

    <f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:form.legend" />
    Copied!

Domain attribute priority 

When both domain and extensionName are provided, the domain attribute takes precedence:

<!-- Uses "other_extension.messages" domain, not "MyExtension" -->
<f:translate key="label" domain="other_extension.messages" extensionName="MyExtension" />
Copied!

Automatic domain detection 

If neither domain nor extensionName are specified, the ViewHelper attempts to automatically detect the translation domain from the context:

  1. Extbase context: Uses the controller extension name
  2. Key with domain prefix: Extracts domain from key="domain:id"
  3. LLL reference: Parses the extension key from the file path

Examples 

Using domain attribute with full domain name 

You can specify the exact translation domain including resource names:

<f:translate key="menu.item" domain="backend.toolbar" />
<!-- Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf -->
Copied!

Inline domain syntax in key 

The shortest form combines domain and key directly:

<f:translate key="core.form.tabs:general" />
<!-- Resolves to: EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf -->
Copied!

With arguments and default values 

All existing ViewHelper features work with the new syntax:

<f:translate
    key="users"
    domain="backend.messages"
    arguments="{0: count}"
    default="No users found"
/>
Copied!

Using variables for domain 

Dynamic domain selection is supported:

<f:translate key="label.title" domain="{myDomain}" />
Copied!

Migration from extensionName 

Existing code using extensionName continues to work without changes. However, new code should prefer the domain attribute combined with translation domain syntax for better readability.

Before:

<f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang_form.xlf:legend" />
<f:translate key="legend" extensionName="MyExtension" />
Copied!

After:

<f:translate key="my_extension.form:legend" />
<f:translate key="legend" domain="my_extension.form" />
Copied!

Impact 

The <f:translate> ViewHelper now provides a more convenient and readable way to reference translations using the new translation domain syntax. This reduces verbosity in Fluid templates and aligns with modern translation system conventions used in Symfony and other frameworks.

All existing syntax forms remain fully supported, ensuring backward compatibility. The new syntax can be adopted incrementally within a project, and both old and new forms can coexist in the same template.

Feature: #107783 - Register metadata extractors via interface 

See forge#107783

Description 

Metadata extractors are service classes that are automatically executed whenever an asset or file is added to the FAL storage, or FAL indexing is executed.

Registration of metadata extractors now happens automatically when the required interface ExtractorInterface is implemented by the class, utilizing autoconfigure tagging provided by the Symfony Dependency Injection framework.

No further manual registration is required.

Additionally, the class ExtractorRegistry now uses strong type declarations, which should not affect public consumers. The interface remains unchanged in its type declarations.

Impact 

Instances of ExtractorInterface are now detected and registered automatically.

Feature: #107784 - Autoconfigure backend layout data providers 

See forge#107784

Description 

Backend layout providers are now autoconfigured once they implement the required DataProviderInterface . Each autoconfigured layout provider is tagged with page_layout.data_provider in the service container and is automatically added to the global DataProviderCollection , if autoconfiguration is enabled in Services.yaml or Services.php.

Since backend layout providers must be identifiable to establish a relation to a configured backend layout, the corresponding interface has been extended. It now requires backend layout providers to implement a new method getIdentifier().

Example 

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

final class MyLayoutDataProvider implements DataProviderInterface
{
    // ...

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

Manual service configuration 

If autoconfiguration is disabled, manually tag the service in Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
Copied!

Provider ordering 

If you need to control the order in which providers are processed, use service priorities in your Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
        priority: 100
Copied!

Impact 

Backend layout data providers are now automatically registered and can be used without further configuration. This improves developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] can no longer be used. Instead, existing backend layout providers must implement the new method getIdentifier().

Using the new autoconfigure-based approach, developers can still support multiple TYPO3 Core versions by keeping the legacy array-based approach next to the new autoconfigure-based configuration.

Feature: #107791 - Reports module uses native submodule overview 

See forge#107791

Description 

The Administration > Reports module has been refactored to use TYPO3 Core’s native submodule system with the showSubmoduleOverview feature instead of a custom report registration mechanism. This provides a more consistent user experience and aligns the Reports module with other TYPO3 backend modules.

The module now displays a card-based overview of available reports, similar to other modules like the Content > Status module. Each report is registered as a proper backend submodule in Configuration/Backend/Modules.php.

Changes 

Module structure:

  • Administration > Reports is now a container module with showSubmoduleOverview enabled.
  • Individual reports (Status, Record Statistics) are registered as submodules.
  • The native submodule overview automatically provides cards with icons, titles, and descriptions.

Benefits:

  • Automatic "Module Overview" menu item in the submodule dropdown.
  • Consistent navigation experience across all TYPO3 backend modules.
  • Simpler architecture without custom registration infrastructure.
  • Easier to extend - just add submodules to Modules.php.

Example 

Creating a custom report now follows the same mechanism as registering a backend submodule:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'system_reports_myreport' => [
        'parent' => 'system_reports',
        'access' => 'admin',
        'path' => '/module/system/reports/myreport',
        'iconIdentifier' => 'my-report-icon',
        'labels' => 'my_extension.module',
        'routes' => [
            '_default' => [
                'target' => \MyVendor\MyExtension\Controller\MyReportController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The controller returns a standard PSR-7 response with rendered content:

EXT:my_extension/Classes/Controller/MyReportController.php
use TYPO3\CMS\Core\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

#[AsController]
final readonly class MyReportController
{
    public function __construct(
        protected ModuleTemplateFactory $moduleTemplateFactory,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->moduleTemplateFactory->create($request);
        $view->assign('data', $this->collectReportData());
        return $view->renderResponse('MyReport');
    }
}
Copied!

Impact 

The Administration > Reports module now provides a cleaner and more intuitive interface using standard TYPO3 backend navigation patterns.

Extension developers can create custom reports by registering them as backend submodules, using the same module registration mechanisms already available in TYPO3, instead of implementing a separate custom report interface.

Feature: #107794 - Improved breadcrumb navigation in backend 

See forge#107794

Description 

The TYPO3 backend now provides contextual breadcrumb navigation in the document header of backend modules, helping users understand their current location and navigate back through hierarchies.

Breadcrumbs are automatically displayed when:

  • Editing records (pages, content elements, etc.)
  • Creating new records
  • Browsing file storages and folders
  • Working with multiple records simultaneously

The breadcrumb navigation includes the following features:

Smart context detection
Breadcrumbs automatically adapt based on what you are working with — whether it is a page, content element, file, or folder.
Hierarchical navigation
Click any breadcrumb item to navigate back to that level in the hierarchy. For pages, the complete page tree path is shown.
Module awareness
Breadcrumbs remember which module you are in and keep you in that module when navigating (for example, staying in the Info module instead of switching to the Page module).
Route preservation
When navigating through breadcrumbs, the current module action or sub-route is preserved (for example, remaining in edit view when clicking parent pages).
Responsive design
On smaller screens, breadcrumb items automatically collapse into a dropdown to save space while maintaining full functionality.

Impact 

Backend users benefit from improved navigation and orientation:

  • Always know where you are: The breadcrumb trail shows your current location in the page tree, file system, or record hierarchy.
  • Quick navigation: Jump back to any parent level with a single click instead of using the browser’s back button or tree navigation.
  • Context preservation: Stay within your current module when navigating through parent items.
  • Special states visible: When creating new records or editing multiple items, this is clearly indicated in the breadcrumb trail.

Examples 

Page editing
When editing page Contact in a site structure like Home → Company → Contact, the breadcrumb shows: HomeCompanyContact
Content creation
When creating a new content element on page About, the breadcrumb shows: HomeAboutCreate New Content Element
File management
When browsing fileadmin/images/products/ the breadcrumb shows: fileadminimagesproducts

For extension developers 

Setting basic breadcrumb context 

Custom backend modules can integrate breadcrumb navigation using new convenience methods on DocHeaderComponent :

// For page-based modules
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For record editing
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);

// For file/folder browsing
$view->getDocHeaderComponent()->setResourceBreadcrumb($file);
Copied!

These methods automatically generate appropriate breadcrumb trails including:

  • Page tree hierarchy for page-based modules
  • Parent pages for content records
  • Folder structure for file resources
  • Module hierarchy for third-level modules

Adding suffix nodes for special states 

The addBreadcrumbSuffixNode() method allows appending custom breadcrumb nodes after the main breadcrumb trail. This is useful for indicating special states or actions such as:

  • “Create New” actions when creating records
  • “Edit Multiple” states when editing multiple records
  • Custom contextual information specific to the current view

Example: Adding a "Create New" suffix node

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$view = $this->moduleTemplateFactory->create($request);
$docHeader = $view->getDocHeaderComponent();

// Set main breadcrumb context (for example current page)
$docHeader->setPageBreadcrumb($pageInfo);

// Add suffix node for "Create New" action
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'new',
        label: 'Create New Content Element',
        icon: 'actions-add'
    )
);
Copied!

Example: Multiple suffix nodes

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$docHeader = $view->getDocHeaderComponent();
$docHeader->setRecordBreadcrumb('pages', $pageUid);

// First suffix: editing mode
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'edit',
        label: 'Edit',
        icon: 'actions-document-open'
    )
);

// Second suffix: specific field
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'field',
        label: 'Page Properties'
    )
);
Copied!

Example: Clickable suffix nodes

Suffix nodes can also be clickable by providing a URL:

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute(
    'web_layout',
    ['id' => $pageUid, 'mode' => 'preview']
);

$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'preview',
        label: 'Preview Mode',
        icon: 'actions-view',
        url: $url
    )
);
Copied!

Deprecation notice 

The previous setMetaInformation() method has been deprecated in favor of the new breadcrumb API. See Deprecation: #107813 - Deprecate MetaInformation API in DocHeader for migration instructions.

Feature: #107795 - Introduce Integrations module 

See forge#107795

Description 

The new Integrations module has been introduced in the TYPO3 backend under the Administration section. This module serves as the central hub for connecting TYPO3 with external systems and third-party services.

The module uses the card-based submodule overview feature to provide an intuitive and organized interface for managing different types of integrations. At present, it consolidates the following existing modules as third-level submodules:

  • Webhooks - Manage outgoing HTTP webhooks to external systems
  • Reactions - Manage incoming HTTP webhooks from external systems

The Integrations module uses a three-level hierarchy structure:

  • Administration (main module)
  • Integrations (second-level parent module with card-based overview)
  • Webhooks / Reactions (third-level modules)

Module navigation 

The third-level modules (Administration > Integrations > Webhooks and :guilabel:`Administration > Integrations > Reactions) now include:

  • Doc header module menu - Quick navigation dropdown to switch between submodules or return to the Integrations overview
  • Go back button - Direct link to return to the Integrations overview

Backward compatibility 

The existing module identifiers continue to work through aliases:

  • webhooks_management redirects to integrations_webhooks
  • system_reactions redirects to integrations_reactions

Impact 

The new Administration > Integrations module provides a centralized location for managing all types of external system integrations in TYPO3. This improves backend organization and user experience by grouping related functionality together.

The module is designed to be extensible, allowing future integration types, such as translation services, AI platforms, or other external tools, to be added as additional third-level modules within the Integrations hub.

Feature: #107812: Setting to restrict Latest changed Pages Widget to current user's changes 

See forge#107812

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing Latest changed Pages widget can now be configured to display only pages that have been changed by the current TYPO3 user.

Only show my changes

Impact 

  • TYPO3 users can use the Latest changed Pages widget to quickly resume their recent work.
  • Existing setups are unaffected unless this option is explicitly enabled.

Feature: #107823 - ComponentFactory for backend components 

See forge#107823

Description 

A new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class has been introduced as the central location for all backend component creation. It provides factory methods for buttons and menu components, offering both pre-configured buttons for common patterns and basic component creation methods.

The ComponentFactory serves multiple purposes:

  1. Pre-configured common buttons – Ready-to-use buttons like back, close, save, reload, and view with standardized icons, labels, and behavior.
  2. Basic button creation – Factory methods for creating button instances (previously available only on ButtonBar).
  3. Menu component creation – Factory methods for creating Menu and MenuItem instances (previously available only on MenuRegistry and Menu).

The deprecated ButtonBar::make*(), Menu::makeMenuItem(), and MenuRegistry::makeMenu() methods have been replaced by ComponentFactory, providing a cleaner separation of concerns where container classes manage organization and ComponentFactory handles component creation.

Additionally, several "add" methods now support fluent interface patterns to enable method chaining for improved code readability.

Database record list and file list 

The \TYPO3\CMS\Backend\RecordList\DatabaseRecordList and \TYPO3\CMS\Filelist\FileList classes, which are responsible in the backend to show all lists of records and files, now make use of the ComponentFactory. The list of "action buttons" is no longer represented with plain HTML and allows a clear distinction of primary and secondary actions.

For this, the new enum \TYPO3\CMS\Backend\Template\Components\ActionGroup and \TYPO3\CMS\Backend\Template\Components\ComponentGroup are also added to the Button API to allow this grouping.

The existing PSR-14 events ( \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent , \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent ) which are used within these classes have been streamlined to deal with these API changes. Details can be found in the related breaking changes document of forge#107884.

The Button API has also been enhanced to allow passing a new \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum to differentiate the buttons for certain icon sizes.

Available Factory Methods 

The ComponentFactory provides two categories of methods:

Pre-configured common buttons:

These methods provide ready-to-use buttons with sensible defaults. The returned instances are fully mutable and can be further customized using fluent interface methods (for example, setDataAttributes(), setClasses(), setIcon()).

URL parameters accept both string and UriInterface for convenience.

  • createBackButton(string|UriInterface $returnUrl) – Standard back navigation with "Go back" label.
  • createCloseButton(string|UriInterface $closeUrl) – Close button for modal-like views.
  • createSaveButton(string $formName = '') – Standard save button for forms.
  • createReloadButton(string|UriInterface $requestUri) – Reload current view.
  • createViewButton(array $previewDataAttributes = []) – View/preview page button with data attributes.

Basic button creation:

  • createLinkButton() – Creates a new LinkButton instance.
  • createInputButton() – Creates a new InputButton instance.
  • createGenericButton() – Creates a new GenericButton instance.
  • createSplitButton() – Creates a new SplitButton instance.
  • createDropDownButton() – Creates a new DropDownButton instance.
  • createFullyRenderedButton() – Creates a new FullyRenderedButton instance.
  • createShortcutButton() – Creates a new ShortcutButton instance.
  • createDropDownDivider() – Creates a new DropDownDivider instance.
  • createDropDownItem() – Creates a new DropDownItem instance.

Menu component creation:

  • createMenu() – Creates a new Menu instance.
  • createMenuItem() – Creates a new MenuItem instance.

The Button API has also been enhanced to allow passing a new enum to differentiate the buttons for certain icon sizes.

Improvements to Button API types 

The following button types can use getSize() and setSize() methods in their instance to set the icon size with the \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum, choosing between a small and medium variant (utilizing CSS classes internally):

  • \TYPO3\CMS\Backend\Template\Components\Buttons\DropDownButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton

Impact 

Backend module developers should now inject ComponentFactory in their controllers to create buttons. The factory provides:

  1. Pre-configured buttons for common patterns (back, close, save, reload, view).
  2. Basic button creation methods (formerly only available on ButtonBar).

The ButtonBar::make*() methods continue to work but are deprecated and will be removed in TYPO3 v15. This change provides a cleaner architecture where ComponentFactory handles all button creation and ButtonBar focuses solely on managing button positioning and organization.

Example – Using pre-configured buttons (inject ComponentFactory):

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// In controller constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// In controller action
public function editAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    // Use pre-configured back button
    $backButton = $this->componentFactory->createBackButton($returnUrl);
    $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1);

    // Use pre-configured save button
    $saveButton = $this->componentFactory->createSaveButton('editform');
    $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);

    // ...
}
Copied!

Example – Creating basic buttons via ComponentFactory :

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Inject ComponentFactory in constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// Create basic buttons via factory
$linkButton = $this->componentFactory->createLinkButton()
    ->setHref($url)
    ->setTitle('Custom')
    ->setIcon($icon);
Copied!

Example - Using the ModuleTemplate convenience method:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Shorthand: use ModuleTemplate::addButtonToButtonBar()
    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createReloadButton(
            $this->request->getUri()->getPath()
        ),
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createBackButton($returnUrl),
        ButtonBar::BUTTON_POSITION_LEFT,
        1
    );

    // ...
}
Copied!

Example - Customizing pre-configured buttons:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Pre-configured buttons return mutable instances that can be further
    // customized.
    $reloadButton = $this->componentFactory
        ->createReloadButton(
            (string)$this->uriBuilder->buildUriFromRoute(
                $currentModule->getIdentifier()
            )
        )
        ->setDataAttributes(['csp-reports-handler' => 'refresh']);

    $this->moduleTemplate->addButtonToButtonBar(
        $reloadButton,
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    // Add custom styling or behavior to a save button
    $saveButton = $this->componentFactory
        ->createSaveButton('myform')
        ->setClasses('btn-primary custom-save')
        ->setDataAttributes(['validate' => 'true']);

    $this->moduleTemplate->addButtonToButtonBar(
        $saveButton,
        ButtonBar::BUTTON_POSITION_LEFT
    );

    // URL parameters accept both string and UriInterface
    $backButton = $this->componentFactory->createBackButton(
        $this->request->getUri()
    ); // UriInterface
    // or
    $backButton = $this->componentFactory->createBackButton(
        '/return/url'
    ); // string

    // ...
}
Copied!

Fluent Interface Improvements 

Several "add" methods now support fluent interface patterns, enabling method chaining:

ButtonBar

$buttonBar
    ->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1)
    ->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
Copied!

Menu

$menu
    ->addMenuItem($listItem)
    ->addMenuItem($gridItem)
    ->addMenuItem($tableItem);
Copied!

MenuRegistry

$menuRegistry
    ->addMenu($viewMenu)
    ->addMenu($filterMenu);
Copied!

These changes provide a more fluent API while maintaining backward compatibility, as the return values were previously ignored ( void).

Design Rationale 

Why fluent interface instead of named parameters?

The ComponentFactory intentionally uses a fluent interface approach (chained method calls) rather than accepting parameters in factory methods. This design decision was made for several important reasons:

Consistency with TYPO3 patterns
The fluent interface pattern is well-established throughout TYPO3's codebase and familiar to extension developers. Introducing a different pattern here would be inconsistent with the rest of the framework.
Diverse button types with different properties
Different button types have vastly different configuration requirements. For example, InputButton needs name/value/form attributes, LinkButton needs href attributes, DropDownButton needs items, and SplitButton needs primary and secondary actions. A unified parameter-based approach does not fit this diversity well and would lead to confusing method signatures with many optional parameters.
Pre-configured buttons solve common cases
The factory already provides pre-configured methods like createSaveButton(), createBackButton(), createCloseButton(), createReloadButton(), and createViewButton() that handle the most common use cases with minimal code.
Avoids duplication and maintenance burden
A parameter-based approach would require duplicating all button-specific configuration knowledge in both the button classes and the factory methods. This creates a maintenance burden where changes to a button's properties must be reflected in multiple locations.
Keeps factory simple and maintainable
By keeping factory methods focused on instantiation, the ComponentFactory remains simple, maintainable, and easy to extend. Each button class maintains complete ownership of its own configuration logic.

Fluent interface is already concise

// Common case – use pre-configured button
$saveButton = $this->componentFactory->createSaveButton('myform');

// Custom case - fluent interface is clear and flexible
$customButton = $this->componentFactory->createInputButton()
    ->setName('custom_action')
    ->setValue('1')
    ->setTitle('Custom Action')
    ->setIcon(
        $this->iconFactory->getIcon('actions-heart', IconSize::SMALL)
    );
Copied!

This design ensures the API remains maintainable, consistent with TYPO3 conventions and suitable for the diverse requirements of different button types while still providing convenience methods for common patterns.

Feature: #107871 - Autoconfigure backend avatar providers 

See forge#107871

Description 

Backend avatar providers must either use the PHP attribute #[AsAvatarProvider] or be manually tagged in the service container with backend.avatar_provider.

When autoconfiguration is enabled in Services.yaml or Services.php, applying #[AsAvatarProvider] will automatically add the backend.avatar_provider tag. Otherwise, the tag must be configured manually.

Example 

EXT:my_extension/Classes/Backend/Avatar/MyAvatarProvider.php
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
use TYPO3\CMS\Backend\Backend\Avatar\AvatarProviderInterface;

#[AsAvatarProvider(
    'my_provider',
    before: ['provider-1'],
    after: ['provider-2']
)]
final class MyAvatarProvider implements AvatarProviderInterface
{
    // ...
}
Copied!

Impact 

Backend avatar providers are now automatically registered using the PHP attribute #[AsAvatarProvider]. This improves the developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] can no longer be used.

To support multiple TYPO3 Core versions simultaneously, extensions may still implement the legacy array-based registration alongside the new autoconfiguration-based approach.

Feature: #107873 - New Bookmarks Widget 

See forge#107873

Description 

A new Bookmarks dashboard widget has been added to display the current user's bookmarks directly in the TYPO3 Dashboard. This allows editors and administrators to quickly access frequently used pages, records, or modules without navigating through the backend menu.

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the Bookmarks widget supports the following settings:

Widget Label
Custom title for the widget instance. If left empty and a specific group is selected, the group title is used as widget label automatically
Group
Filter bookmarks by a specific bookmark group, or show all groups.
Limit
Maximum number of bookmarks to display (default: 10).

Impact 

  • Editors can add the Bookmarks widget to their dashboard for quick access to their saved bookmarks.
  • Each widget instance can be configured independently, allowing multiple Bookmarks widgets with different group filters on the same dashboard.
  • Existing bookmarks are displayed automatically without additional setup.

Feature: #107875 - Improved DocHeader layout and unified language selector 

See forge#107875

Description 

The TYPO3 backend document header (DocHeader) has been reorganized to provide a more consistent and intuitive user experience across all backend modules.

This feature includes the following improvements:

Unified language selector 

All backend modules with multi-language support now use a consistent language selector in the top-right area of the DocHeader. This includes:

  • Content > Layout
  • Content > List
  • Content > Preview
  • Record editing (FormEngine)

The language selector displays the currently selected language as the button text (for example "English", "German", and the special All languages) with a descriptive "Language:" prefix for screen readers. This provides immediate visual feedback about the active language while maintaining full accessibility.

The language selector now allows creating new page translations directly from the dropdown (except in Preview mode). The dropdown is organized with existing translations shown first, followed by a divider and a "Create new translation" section header. Languages that do not yet have a translation appear in this separate section.

Selecting a language from the "Create new translation" section will:

  1. Create the page translation via the DataHandler.
  2. Open the FormEngine to edit the newly created translation.
  3. Provide a consistent workflow across all modules.

Unified module actions menu 

Modules with submodules or multiple "actions" now consistently display these options as a dropdown button in the DocHeader button bar. The dropdown button displays the currently active module or action as the button text, providing clear visual context to users.

For accessibility, each dropdown includes a descriptive label:

  • "Display mode" for the Content > Layout module view selector (layout vs language comparison)
  • "Module actions" for general module or submodule navigation
  • Custom labels defined by individual menu configurations

These labels are implemented using visually hidden label elements, ensuring screen readers announce both the dropdown's purpose (for example, "Display mode:") and the current selection (for example, "Layout").

The dropdown is automatically hidden when only a single action is available, reducing visual clutter and showing the dropdown only when navigation choices are actually available.

Examples include Administration > Users with Overview, Users, Groups actions, as well as Content > Status with "Pagetree Overview", "Localization Overview", and more.

This replaces the previous inconsistent mixture of:

  • Back buttons
  • Inline button groups
  • Module menus in different locations

Reorganized DocHeader layout 

The DocHeader now has a more logical structure:

Top row (navigation bar):

  • Left side: Breadcrumb navigation showing the current location
  • Right side: Language selector (when applicable)

Second row (button bar):

  • Left side, group 0: Module actions dropdown (when applicable)
  • Left side, groups 1+: Additional action buttons and context-specific buttons (Save, Close, and so on)
  • Right side: Functional buttons such as "Reload", "Bookmark", or "Clear cache"

Technical details 

Module actions dropdown 

Controllers can use the existing ModuleTemplate::makeDocHeaderModuleMenu() method to automatically create a module actions dropdown based on the module configuration:

$view = $this->moduleTemplateFactory->create($request);
$view->makeDocHeaderModuleMenu();
Copied!

For backward compatibility, the MenuRegistry API is automatically converted to module actions dropdowns.

Language selector integration 

The language selector is integrated via the DocHeaderComponent::setLanguageSelector() method.

First, inject the \TYPO3\CMS\Backend\Template\Components\ComponentFactory into your controller:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    private readonly ComponentFactory $componentFactory,
) {}
Copied!

Then build the language selector dropdown with language items:

// Get available site languages for the current page
$siteLanguages = $this->site->getAvailableLanguages($pageRecord, $backendUser);
$currentLanguageId = (int)$moduleData->get('language');

// Build dropdown button
$languageDropdown = $this->componentFactory->createDropDownButton()
    ->setLabel($languageService->sL('core.core:labels.language'))
    ->setShowLabelText(true)
    ->setShowActiveLabelText(true);

// Create dropdown items for each language
foreach ($siteLanguages as $siteLanguage) {
    $languageId = $siteLanguage->getLanguageId();
    $isActive = $currentLanguageId === $languageId;

    // Build URL for language selection
    $href = $this->uriBuilder->buildUriFromRoute('web_layout', [
        'id' => $pageId,
        'language' => $languageId,
    ]);

    $item = $this->componentFactory->createDropDownRadio()
        ->setHref($href)
        ->setLabel($siteLanguage->getTitle())
        ->setIcon(
            $this->iconFactory->getIcon($siteLanguage->getFlagIdentifier())
        )
        ->setActive($isActive);

    $languageDropdown->addItem($item);
}

$view->getDocHeaderComponent()->setLanguageSelector($languageDropdown);
Copied!

The language selector component is automatically rendered in the top-right area of the DocHeader navigation bar.

Accessibility implementation 

The setShowActiveLabelText(true) method enables display of the active item's label while maintaining accessibility. When enabled with setShowLabelText(true), the button renders the dropdown's label in a visually hidden span followed by the active item's label:

  • Visual users see: "English" (active language)
  • Screen reader users hear: "Language: English" (descriptive label + active language)

When setShowLabelText(false), the accessibility information is provided through aria-label and title attributes instead.

This pattern applies to all dropdown buttons in the DocHeader:

$dropdown->setLabel('Display mode')  // Descriptive label for context
    ->setShowLabelText(true)         // Show label text
    ->setShowActiveLabelText(true);  // Show active item label
Copied!

Impact 

The changes provide a more consistent and intuitive user experience. Users can now create and switch between page translations directly from the DocHeader across all relevant modules, without needing to navigate to specific page areas. The clear separation between existing translations and languages that can be created improves discoverability and reduces confusion about which actions are available.

The language selector and module actions dropdowns now display the currently active selection as the button text, providing immediate visual feedback. Users can see at a glance which language is selected or which module action is active, without needing to open the dropdown.

Accessibility has been significantly improved through the use of descriptive labels rendered as visually hidden elements or aria-label attributes. Screen reader users now receive proper context about each dropdown's purpose (e.g., "Language:", "Display mode:", "Module actions") combined with the current selection, ensuring equal access to navigation functionality.

The module actions dropdown makes it immediately clear which module or action is currently active, and which alternatives are available. The dropdown is only shown when multiple options exist, reducing visual clutter. Removal of redundant back buttons in favor of consistent breadcrumb navigation further reduces visual noise.

Feature: #107889 - Introduce TCA option "itemsProcessors" 

See forge#107889

Description 

As part of centralizing and improving the processing of items for select, check, and radio type fields, a new TCA option itemsProcessors has been introduced as a replacement for itemsProcFunc. This option is an array, allowing any number of processors to be called instead of just one. Processors are ordered by their array key (numerical) and executed in that order. Processors can receive arbitrary data through the $context->processorParameters property.

All processors must implement the ItemsProcessorInterface interface.

Processor methods receive two parameters: a SelectItemCollection instance containing the items, and an ItemsProcessorContext instance providing access to table, field, row data, and configuration. The processor must return a SelectItemCollection. This means that added items can no longer be untyped arrays, making the entire process cleaner and safer.

A new Page TSconfig option is also available, mirroring the existing one for itemsProcFunc. See the example below for syntax.

Example 

The TCA registration might look like this:

EXT:my_extension/Configuration/TCA/my_table.php
 use MyVendor\MyExtension\Processors\SpecialRelationsProcessor;

'relation' => [
    'label' => 'Relational field',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectSingle',
        'items' => [
            [
                'value' => 0,
                'label' => '',
            ],
        ],
        'foreign_table' => 'some_foreign_table',
        'itemsProcessors' => [
            100 => [
                'class' => SpecialRelationsProcessor::class,
                'parameters' => [
                    'foo' => 'bar',
                ],
            ],
            50 => [
                'class' => SpecialRelationsProcessor2::class,
            ],
        ],
    ],
],
Copied!

In this example, SpecialRelationsProcessor2 will be called before SpecialRelationsProcessor.

Here is an example processor:

EXT:my_extension/Classes/Processors/SpecialRelationsProcessor.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Processors;

use TYPO3\CMS\Core\DataHandling\ItemsProcessorContext;
use TYPO3\CMS\Core\DataHandling\ItemsProcessorInterface;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Schema\Struct\SelectItemCollection;

final class SpecialRelationsProcessor implements ItemsProcessorInterface
{
    public function processItems(
        SelectItemCollection $items,
        ItemsProcessorContext $context,
    ): SelectItemCollection {
        $items->add(
            new SelectItem(
                type: 'select',
                label: 'Extra item',
                value: 42,
            )
        );

        return $items;
    }
}
Copied!

The $context->processorParameters property contains any parameters defined in the TCA declaration.

Using Page TSconfig to pass custom parameters to the processor would look like this:

TCEFORM.example_table.content.itemsProcessors.100.foo = bar
Copied!

Note that the numerical key of the processor must be reused. With this setup, the class SpecialRelationsProcessor receives the PHP array ['foo' => 'bar'] in the $context->fieldTSconfig property. The class SpecialRelationsProcessor2 receives an empty array [] (since it is registered with the key 50).

Registration of processors is also possible within FlexForms:

EXT:my_package/Configuration/FlexForms/SomeForm.xml
<some_selector>
    <label>Choice</label>
    <config>
        <type>select</type>
        <renderType>selectSingle</renderType>
        <itemsProcessors>
            <numIndex index="100">
                <class>MyVendor\MyPackage\Processors\SpecialRelationsProcessor</class>
            </numIndex>
        </itemsProcessors>
    </config>
</some_selector>
Copied!

Impact 

It is still possible to use itemsProcFunc, but switching to itemsProcessors is recommended because it offers two main advantages:

  • Being an array, it allows extensions to add processors on top of existing ones and to define their execution order.
  • The processing chain is strictly typed, ensuring safer and more reliable code.

If both itemsProcFunc and itemsProcessors are defined, both are executed, with itemsProcFunc executed first.

Feature: #107953 - Add "Current logged-in users" filter to the "Backend Users" module 

See forge#107953

Description 

A new filter option Current logged-in users has been added to the Administration > Users module. This feature allows administrators to quickly list all backend accounts that are currently active.

The detection mechanism is based on the lastlogin timestamp in combination with the global configuration value $GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] , which defines the maximum lifetime of backend user sessions.

Impact 

Administrators can now filter backend users by those who are currently logged in, providing an immediate overview of active sessions directly within the Administration > Users module.

Feature: #108002 - Introduce built-in symmetric encryption/decryption cipher service 

See forge#108002

Description 

TYPO3 now provides a built-in symmetric encryption and decryption service using the modern XChaCha20-Poly1305 AEAD (Authenticated Encryption with Associated Data) cipher. This service allows extensions and core code to securely encrypt sensitive data such as API tokens, passwords, or personal information without implementing custom encryption solutions or relying on third-party libraries.

Benefits 

The new \TYPO3\CMS\Core\Crypto\Cipher\CipherService provides several advantages:

  • Secure by default: Uses modern, cryptographically secure algorithms provided by the libsodium library (ext-sodium), which is available by default in all PHP versions currently supported by TYPO3.
  • Seamless integration: Secret keys can be derived from TYPO3's existing $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] , eliminating the need to manage additional encryption keys.
  • Authenticated encryption: The AEAD cipher not only encrypts data but also ensures its integrity, preventing tampering.
  • Application-specific keys: Different components can derive isolated encryption keys using seed values, enhancing security through key separation.

Key Derivation 

Secret keys are derived from the existing TYPO3 encryption key using the \TYPO3\CMS\Core\Crypto\Cipher\KeyFactory . This approach leverages the master encryption key that is already configured in your TYPO3 installation, ensuring that different applications or contexts can have their own isolated keys while using the same master key.

Please note that to decrypt data in the future, the encryptionKey must not be changed and must remain available.

Example for deriving a secret key:

use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get the KeyFactory via dependency injection
public function __construct(
    private readonly KeyFactory $keyFactory
) {}

// Derive an application-specific secret key from the TYPO3 encryption key
// The seed 'my-extension-tokens' identifies this specific use case
$secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens');

// You can also derive multiple sub-keys from the same seed
$secretKey1 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 1);
$secretKey2 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 2);
Copied!

Each unique seed produces a different encryption key, allowing for key separation between different features or data types within your extension.

Key Usage/Generation 

Instead of deriving a key, it is also possible to generate a new key from a random value or to use a provided key (e.g., via environment variables).

  • $this->keyFactory->createSharedKeyFromString(getenv('MY_APP_KEY'))
  • $this->keyFactory->generateSharedKey()

Encryption Example 

To encrypt sensitive data, use the CipherService::encrypt() method:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function encryptApiToken(string $apiToken): string
{
    // Derive a secret key for API token encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Encrypt the API token
    $cipherValue = $this->cipherService->encrypt($apiToken, $secretKey);

    // Convert to string for storage in database
    // The result has the format: {base64url-encoded-json}
    // Example: "eyJub25jZSI6IlxcXHUwMDEy4oCmIiwiY2lwaGVyIjoi4oCmIn0"
    // This string contains both the nonce and ciphertext in a JSON structure
    $encryptedString = (string)$cipherValue;

    return $encryptedString;
}
Copied!

Each encryption generates a unique result due to the random nonce, even when encrypting the same plaintext multiple times.

Decryption Example 

To decrypt data, use the CipherService::decrypt() method with the same secret key that was used for encryption:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\CipherValue;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;
use TYPO3\CMS\Core\Crypto\Cipher\CipherDecryptionFailedException;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function decryptApiToken(string $encryptedToken): string
{
    // Derive the same secret key used during encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Parse the cipher text (format: {base64url-encoded-json})
    $cipherValue = CipherValue::fromSerialized($encryptedToken);

    try {
        // Decrypt the token
        $apiToken = $this->cipherService->decrypt($cipherValue, $secretKey);
        return $apiToken;
    } catch (CipherDecryptionFailedException $e) {
        // Decryption failed - wrong key, tampered data, or invalid format
        throw new \RuntimeException(
            'Failed to decrypt API token: ' . $e->getMessage(),
            1762465682,
            $e
        );
    }
}
Copied!

Decryption will fail if the wrong key is used, the data has been tampered with, or the ciphertext format is invalid.

Impact 

The CipherService and KeyFactory are now available as autoconfigured services throughout TYPO3 core and extensions. This provides a standardized, secure way to encrypt and decrypt sensitive data without requiring additional dependencies beyond ext-sodium, which is already a requirement of TYPO3 core.

Extensions can now securely store encrypted data in the database or configuration files using this built-in service, ensuring consistent security practices across the TYPO3 ecosystem.

Feature: #108008 - Automatic reload and shortcut buttons in backend modules 

See forge#108008

Description 

Backend modules now automatically get reload and shortcut buttons added to their document header, ensuring consistent display across all backend modules.

Previously, controllers manually added these buttons with varying group numbers, leading to inconsistent positioning. Now, buttons always appear on the right side, ensuring they are always the last two buttons regardless of what other buttons are added.

Controllers provide shortcut information using the new DocHeaderComponent::setShortcutContext() method:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'site_configuration.edit',
    displayName: sprintf('Edit site: %s', $siteIdentifier),
    arguments: ['site' => $siteIdentifier]
);
Copied!

Buttons are automatically added during rendering before the PSR-14 ModifyButtonBarEvent is dispatched, allowing event listeners to modify or remove them if needed.

Controllers can disable automatic buttons if custom behavior is required:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->disableAutomaticReloadButton();
$view->getDocHeaderComponent()->disableAutomaticShortcutButton();
Copied!

Impact 

Reload and shortcut buttons now appear consistently at the same position across all backend modules, providing a predictable user experience.

Controllers no longer need to manually create these buttons, reducing boilerplate code. Use DocHeaderComponent::setShortcutContext() to provide shortcut information and remove manual button creation, see Deprecation: #108008 - Manual shortcut button creation.

Before:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;

$reloadButton = $this->componentFactory->createReloadButton($uri);
$view->addButtonToButtonBar(
    $reloadButton,
    ButtonBar::BUTTON_POSITION_RIGHT,
    3
);

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// Set shortcut context only
$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Feature: #108016 - Multi-language selection in page and list modules 

See forge#108016

Description 

The Content > Layout and Content > List modules now support selecting multiple languages simultaneously for improved comparison of translated records.

The language selection state is synchronized between both modules, providing a consistent experience when switching between page and list views.

A fallback mechanism is in place for switching between view modes (Layout and Language Comparison) as well as navigating to a page not available in the current language selection.

Impact 

Users can now:

  • Select multiple specific languages to compare
  • Toggle individual languages on / off
  • Quickly select or deselect all available languages
  • See visual feedback with appropriate icons and counts

The language selection is persisted in module data and shared between the page module and list module for the same page.

Feature: #108027 - Type-specific title in TCA types section 

See forge#108027

Description 

It is now possible to define a type-specific title in the TCA types section. This value overrides the global table title defined in ctrl['title'] for the respective record type.

This allows different record types of the same table to display different titles in the TYPO3 backend user interface, making it clearer which kind of record is being created or displayed.

Implementation 

This feature has been implemented in two areas to ensure consistent behavior:

  1. Schema API The TcaSchemaFactory merges type-specific configuration into sub-schemas using array_replace_recursive(). This makes type-specific titles available through $schema->getTitle().
  2. FormEngine The data provider TcaTypesCtrlOverrides merges the type-specific title and previewRenderer into processedTca['ctrl'] during form rendering. This ensures that both FormEngine and any legacy code accessing ctrl directly will see the correct type-specific values.

Example 

EXT:my_extension/Configuration/TCA/tx_my_table.php
return [
    'ctrl' => [
        'title' => 'my_extension.db:tx_my_table',
        'type' => 'record_type',
        // ... other ctrl configuration
    ],
    'types' => [
        'article' => [
            'title' => 'my_extension.db:tx_my_table.type.article',
            'showitem' => 'title, content, author',
        ],
        'news' => [
            'title' => 'my_extension.db:tx_my_table.type.news',
            'showitem' => 'title, content, publish_date',
        ],
        'event' => [
            // No type-specific title - will use the global ctrl['title']
            'showitem' => 'title, content, event_date',
        ],
    ],
    // ... columns configuration
];
Copied!

In this example:

  • The article type displays the title defined in the language file key tx_my_table.type.article.
  • The news type displays the title defined in tx_my_table.type.news.
  • The event type falls back to the global table title from ctrl['title'].

Impact 

Tables with multiple record types can now define more specific and descriptive titles for each type in the backend user interface. This improves usability and clarity for editors by making it immediately obvious which type of record is being created or edited.

This feature is especially useful for:

  • Content element tables such as tt_content with different CType values
  • Tables with distinct record types serving different purposes
  • Plugin records with varying functionality per type
  • Any table where the record type changes the record's purpose or meaning

Feature: #108049 - Modernized translation workflow 

See forge#108049

Description 

A modernized and extensible translation workflow has been introduced in the TYPO3 backend. The new architecture replaces the previous monolithic localization implementation with a step-based wizard system that guides editors through the translation process. Previously available only in the Content > Layout module, the translation wizard is now the default interface for all translation operations across backend modules.

The backend now consistently opens the new translation wizard whenever users initiate a translation operation, providing a unified experience across all modules. The wizard guides users through the translation process in multiple steps, automatically advancing when no user input is required. This streamlined approach ensures that users only interact with steps requiring configuration or confirmation.

Users can choose between two translation modes: Translate (creates a connected translation) and Copy (creates an independent copy in free mode). To maintain consistency, the wizard only offers the translation mode that matches existing translations for a record, preventing mixed modes.

API for extension developers 

The new LocalizationHandlerRegistry provides a flexible architecture for registering and managing different translation strategies. Handlers implement the LocalizationHandlerInterface and are automatically registered via autoconfiguration with the backend.localization.handler tag. This makes the localization system extensible, allowing custom handlers to be added for specialized translation workflows, such as integration with translation services, AI-powered translation, or custom business logic.

The LocalizationFinisherInterface defines how the wizard completes after a successful translation operation. Finishers can be customized by localization handlers to provide the most appropriate completion behavior for their workflow, such as redirecting to a specific page or reloading the current view.

Custom localization handlers 

Extensions can provide custom localization handlers by implementing the LocalizationHandlerInterface :

EXT:my_extension/Classes/Localization/MyCustomHandler.php
namespace MyVendor\MyExtension\Localization;

use TYPO3\CMS\Backend\Localization\LocalizationHandlerInterface;
use TYPO3\CMS\Backend\Localization\LocalizationInstructions;
use TYPO3\CMS\Backend\Localization\LocalizationMode;
use TYPO3\CMS\Backend\Localization\LocalizationResult;

final class MyCustomHandler implements LocalizationHandlerInterface
{
    public function getIdentifier(): string
    {
        return 'my-custom-handler';
    }

    public function getLabel(): string
    {
        return 'my_extension.messages:handler.label';
    }

    public function getDescription(): string
    {
        return 'my_extension.messages:handler.description';
    }

    public function getIconIdentifier(): string
    {
        return 'my-extension-icon';
    }

    public function isAvailable(LocalizationInstructions $instructions): bool
    {
        // Return true if this handler should be available for the given context
        return $instructions->mainRecordType === 'my_table';
    }

    public function processLocalization(
        LocalizationInstructions $instructions
    ): LocalizationResult {
        // Implement custom localization logic
        // Return a LocalizationResult with success status and finisher
    }
}
Copied!

The handler will be automatically registered via autoconfiguration when it implements LocalizationHandlerInterface . No manual service configuration is required.

Custom finishers 

Extensions can create custom finishers by implementing the LocalizationFinisherInterface and providing a corresponding JavaScript module to handle the frontend logic:

EXT:my_extension/Classes/Localization/Finisher/MyCustomFinisher.php
namespace MyVendor\MyExtension\Localization\Finisher;

use TYPO3\CMS\Backend\Localization\Finisher\LocalizationFinisherInterface;

final class MyCustomFinisher implements LocalizationFinisherInterface
{
    public function getType(): string
    {
        return 'my-custom-finisher';
    }

    public function getModule(): string
    {
        return '@vendor/my-extension/localization/finisher/my-custom-finisher.js';
    }

    public function getData(): array
    {
        return [
            'customData' => 'value',
        ];
    }

    public function getLabels(): array
    {
        return [
            'successMessage' => 'my_extension.messages:finisher.success',
        ];
    }

    public function jsonSerialize(): array
    {
        return [
            'type' => $this->getType(),
            'module' => $this->getModule(),
            'data' => $this->getData(),
            'labels' => $this->getLabels(),
        ];
    }
}
Copied!

The corresponding JavaScript module implements the finisher logic. The finisher is executed in the final step of the wizard and can define custom rendering and behavior. Users can skip the finisher step if no interaction is required.

EXT:my_extension/Resources/Public/JavaScript/localization/finisher/my-custom-finisher.js
import LocalizationFinisher from '@typo3/backend/localization/localization-finisher';
import { html, type TemplateResult } from 'lit';

class MyCustomFinisher extends LocalizationFinisher {
  public render(): TemplateResult {
    // Define what is rendered in the finisher step
    // Can return custom UI elements, messages, actions, etc.
    return html`
      <p>${this.finisher.labels.successMessage}</p>
      <button @click=${() => this.execute()}>
        Perform Action
      </button>
    `;
  }

  public async execute(): Promise<void> {
    // Execute finisher logic (e.g., redirect, reload, show notification)
    // This is called when the wizard completes or when the user clicks
    // the custom action button above
    const customData = this.finisher.data.customData;
    console.log('Finisher executing with data:', customData);

    // Perform your custom finisher action here
    // For example: redirect, reload, show notification, etc.
  }
}

export default MyCustomFinisher;
Copied!

Impact 

The new translation workflow provides a consistent and intuitive user experience across all backend modules. Editors benefit from clear step-by-step guidance through the translation process, with the wizard automatically adapting to the context and showing only relevant options.

Extension developers can register custom localization handlers to integrate specialized translation workflows, such as connections to external translation services or AI-powered translation tools.

Feature: #108148 - Alternative Fluid syntax for CDATA sections 

See forge#108148

Description 

A long-standing issue in Fluid templates has been that the Fluid variable and inline ViewHelper syntax collides with inlined CSS or JavaScript code. This issue has now been addressed with Fluid 5: A new alternative syntax has been introduced that makes collisions between CSS/JavaScript and Fluid far less likely.

The normal inline and variable syntax uses single curly braces { } as tokens in Fluid templates. In <![CDATA[ ]]> sections, this syntax is now ignored. Instead, three curly braces {{{ }}} can be used to call Fluid ViewHelpers or to access variables. The tag-based syntax is disabled altogether in CDATA sections.

Example:

<f:variable name="color" value="red" />
<style>
<![CDATA[
    @media (min-width: 1000px) {
        p {
            background-color: {{{color}}};
        }
    }
]]>
</style>
Copied!

The Fluid Explained documentation contains several practical examples of how this new feature can be used: Avoiding syntax collision with JS and CSS.

Note that it's still considered bad practice to put inline CSS or JavaScript code in Fluid templates. Consider using a dedicated API endpoint, data-* attributes or CSS custom properties (also known as CSS variables) to pass dynamic values to JavaScript and CSS.

Impact 

Inline CSS and JavaScript can now be wrapped in CDATA in Fluid templates, which prevents syntax collision with Fluid's inline syntax.

CDATA sections are thus no longer removed from Fluid templates before rendering, see Breaking: #108148 - CDATA Sections No Longer Removed.

Feature: #108148 - Union types for ViewHelpers 

See forge#108148

Description 

Fluid 5 brings support for union types in ViewHelper argument definitions. Previously, it was necessary to specify an argument as mixed if more than one type should be possible. Now it is possible to specify multiple types separated by a pipe character (|).

The following built-in PHP types can also be used:

  • iterable
  • countable
  • callable

Example:

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class MyViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument(
            'file',
            File::class . '|' . FileReference::class,
            'a file object'
        );
        $this->registerArgument(
            'items',
            'iterable',
            'a list of items'
        );
    }
}
Copied!

This feature also applies to the Argument ViewHelper <f:argument>:

<f:argument name="file" type="TYPO3\CMS\Core\Resource\File|TYPO3\CMS\Core\Resource\FileReference" />
Copied!

Note that union types disable automatic type conversion by Fluid, so it might be necessary to specify more types to keep ViewHelpers flexible. Example:

$this->registerArgument(
    'ids',
    'array|string|int',
    'a list of ids, either comma-separated or as array'
);
Copied!

Impact 

Custom ViewHelper implementations have more options to specify an API with strict type requirements and can avoid manual type checks.

Feature: #108166 - Fluid file extension and template resolving 

See forge#108166

Description 

Fluid 5 introduces a specific file extension for template files. The extension is used in combination with a generic extension, which means that existing syntax highlighting in code editors still works.

Before After
MyTemplate.html MyTemplate.fluid.html
MyTemplate.xml MyTemplate.fluid.xml
... ...

As stated in the Fluid release notes, this new file extension is entirely optional. Existing template files will continue to work. A fallback mechanism is in place, which checks for template files in the following order:

templateRootPath: templates/
template: myTemplate
format: html

1. templates/myTemplate.fluid.html
2. templates/myTemplate.html
3. templates/myTemplate
4. templates/MyTemplate.fluid.html
5. templates/MyTemplate.html
6. templates/MyTemplate
Copied!

This means that *.fluid.* files are preferred over files without the new extension if both files exist in the same folder.

Another noteworthy change in Fluid is that template files no longer need to start with an uppercase character: The user-provided spelling of the template name will be tried first, the uppercase variant will be used as a fallback.

Consequences for template overloading 

If multiple template paths are provided (for example if an extension overloads templates or partials of another extension), this new fallback chain for template file names will be executed per template path. In practice this means the following:

  • A TYPO3 community extension can ship *.html files (without the new file extension), which can be overloaded by a sitepackage extension that already uses *.fluid.html files.
  • A TYPO3 community extension can ship *.fluid.html files, which can still be overloaded by a sitepackage extension that uses *.html files.
  • MyTemplate.html will still overload MyTemplate.html like before, and the same applies to *.fluid.html files.

However, since older Fluid versions do not consider *.fluid.* files, it is not supported to use the new file extension in a TYPO3 community extension that still supports TYPO3 versions below 14.

For the TYPO3 Core this means that all template files can safely be renamed to the new file extension because Fluid 5 is always present. To overload templates from the TYPO3 Core, both the new Fluid file extension and the old file extension can be used, which allows TYPO3 community extensions to remain compatible with multiple TYPO3 Core versions.

Edge case: Templates with file extension specified 

Note that the described file extension fallback chain only works if the file extension is not specified explicitly, but rather derived from the template's format. If the file extension is part of the requested template name, Fluid can't reliably add the *.fluid.* file extension automatically and the template needs to be adjusted.

One use case of this would be a template in format json that calls a partial in format html:

MyTemplate.fluid.json
<!-- Won't work if template file is called MyTemplate.fluid.html: -->
<f:render partial="MyTemplate.html" />

<!-- Needs to be adjusted like this: -->
<f:render partial="MyTemplate.fluid.html" />
Copied!

Impact 

While the TYPO3 Core can already switch to the new *.fluid.* file extension, TYPO3 community extensions will probably continue to use *.html for an extended period. However, on a v14 project, the new file extension can already be used, both for individual development and for the integration of TYPO3 community extensions.

Projects and extension authors willing to switch to the new file extension can use the fluid-rename utility extension, which has already been used for the TYPO3 Core.

This new file extension opens new possibilities because it is now easily recognizable which files will be interpreted by Fluid. This will enable better IDE integration and tooling support in the future.

Feature: #108227 - Allow #[IgnoreValidation] and #[Validate] attributes for method parameters 

See forge#108227

Description 

The Extbase attributes #[IgnoreValidation] and #[Validate] can now be used for controller action method parameters.

This extends the current validation behavior of these attributes, where either (1) the complete method or (2) a single parameter is taken into account for validation. While this works fine for (1), using these attributes in context of (2) raises some concerns regarding

  1. duplication of parameter names,
  2. validation of the existence of a configured parameter, and
  3. unnecessary complexity regarding reflection-based handling of these parameters.

History 

When the attributes were originally implemented as Doctrine annotations, the only possible way to implement behavior (2) was to add the appropriate annotation to the method-related annotation.

Since forge#107229, annotations are no longer used in Extbase, and PHP attributes are the only remaining successor in this area. Since PHP attributes support placement at specific method parameters, the existing attributes can safely rewritten to be placed (1) at a specific method or (2) at a specific method parameter.

Usage with method parameters 

The capabilities of the existing attributes are expanded to allow placement at method parameters as well. The previous behavior (defining validation-related behavior at method level using the existing attribute properties) is now deprecated and will be removed with TYPO3 v15 (see deprecation notice).

Impact 

By allowing the usage of both #[IgnoreValidation] and #[Validate] attributes at method parameter level, the previous error-prone behavior is now hardened. In addition, this change improves developer experience and pushes the Extbase ecosystem towards a modernized architecture.

All installations using these attributes on method level need to migrate these as described in the related section of Deprecation #108227 - Migration.

Feature: #108229 - Fluid cache warmup 

See forge#108229

Description 

TYPO3 v14 leverages the revamped Fluid v5 warmup feature and integrates a Fluid template warmup directly into the CLI command typo3 cache:warmup.

The command finds and compiles all *.fluid.* (for example Index.fluid.html) files found in extensions.

Fluid warmup can also be called directly using typo3 fluid:cache:warmup, which will additionally output compile time deprecations found within Fluid template files.

Verbose output allows to get feedback of warmed up templates and the number of errors/deprecations (or success).

Impact 

The warmup command can be useful to reduce ramp up time after deployments.

Feature: #108240 - Introduce Fresh theme 

See forge#108240

Description 

The new Fresh theme has been introduced for the TYPO3 backend, marking the beginning of broader backend customization capabilities. This theme offers users a modern alternative appearance option with a friendlier purple accent color palette.

Built on the modern CSS architecture, the "Fresh" theme is now the default for new users. It complements the existing Modern and Classic themes. All three themes will be maintained and enhanced equally going forward.

The extensive CSS framework improvements made during the 14.0 cycle now enable easier theme adaptation with minimal code changes, setting the foundation for the expanding theme system.

Theme selection 

Users can select their preferred theme in the User Settings module under the Backend appearance section. The "Fresh" theme provides a more contemporary look and feel while maintaining the familiar TYPO3 backend structure and usability.

All existing themes remain fully supported:

  • Fresh (default for new users) - Modern purple accent colors
  • Modern - Contemporary appearance with traditional accent colors
  • Classic - Traditional TYPO3 backend appearance

Future development 

Additional customization options and modern user interface elements will be incrementally added to the theme system leading up to the long term support release. The modular CSS architecture ensures that all themes benefit from these enhancements equally.

Impact 

The introduction of the "Fresh" theme demonstrates TYPO3's commitment to providing a modern, customizable backend experience. The modular CSS architecture allows for continuous improvement and evolution of the backend design system.

New TYPO3 installations will automatically use the "Fresh" theme, while existing users can opt in through their User Settings. The choice of theme is stored per user, allowing teams with different preferences to work comfortably within the same TYPO3 installation.

Deprecation: #93981 - GraphicalFunctions->gif_or_jpg 

See forge#93981

Description 

Default image formats can now be configured thanks to forge#93981 (see Feature: #93981 - Specify default image conversion processing).

As a result, the method gif_or_jpg() of \TYPO3\CMS\Core\Imaging\GraphicalFunctions is no longer needed.

Fallback behavior for image preview generation now follows the configured file extensions rather than a hardcoded GIF/JPEG switch.

A new method, GraphicalFunctions->determineDefaultProcessingFileExtension(), has been introduced.

It accepts a file extension such as 'pdf' as an argument and returns the corresponding default output extension for preview generation. This method is currently marked as @internal, as it may later be moved to a dedicated service class.

In general, third-party extensions should not determine the output format manually but rely on TYPO3’s built-in image generation APIs.

Deprecated method:

  • \TYPO3\CMS\Core\Imaging\GraphicalFunctions->gif_or_jpg()

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances that directly use the deprecated method.

Migration 

use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$graphicalFunctions = GeneralUtility::makeInstance(GraphicalFunctions::class);

// Before
$filetype = $graphicalFunctions->gif_or_jpg('pdf', 800, 600);
// Returned: 'jpg'

// After
$filetype = $graphicalFunctions->determineDefaultProcessingFileExtension('pdf');
// Returns: 'jpg' (for example, now depends on configuration!)
Copied!

This is a temporary migration using an @internal method, which is subject to change. Code like the above should generally be avoided in third-party extensions.

Instead, use GraphicalFunctions->resize() and specify the argument $targetFileExtension = 'web' so that actual operations use the configured target formats.

Deprecation: #97559 - Deprecate passing an array of configuration values to Extbase attributes 

See forge#97559

Description 

Passing an array of configuration values to Extbase attributes has been deprecated. All configuration values should now be passed as single properties using constructor property promotion. When an array of configuration values is passed for the first available property in an attribute, a deprecation notice will be triggered. The possibility to pass such an array will be removed with TYPO3 v15.

Impact 

The usage of constructor property promotion as an alternative to an array of configuration values enables type safety and value hardening and moves Extbase attributes toward a modern configuration element for models, Data Transfer Objects, and controller actions.

Affected installations 

All installations that make use of Extbase attribute configuration are affected, since this was previously only possible by passing an array of configuration values.

Migration 

Use the available attribute properties instead of an array.

Before: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

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

    #[FileUpload([
        'validation' => [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        'uploadFolder' => '1:/user_upload/files/',
    ])]
    protected ?FileReference $bar = null;
}
Copied!

After: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(validator: 'NotEmpty')]
    protected string $foo = '';

    #[FileUpload(
        validation: [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        uploadFolder: '1:/user_upload/files/',
    )]
    protected ?FileReference $bar = null;
}
Copied!

Combined diff: 

 class MyModel extends AbstractEntity
 {
-    #[Validate(['validator' => 'NotEmpty'])]
+    #[Validate(validator: 'NotEmpty')]
     protected string $foo = '';

-    #[FileUpload([
-        'validation' => [
+    #[FileUpload(
+        validation: [
             'required' => true,
             'maxFiles' => 1,
             'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
             'allowedMimeTypes' => ['image/jpeg', 'image/png'],
         ],
-        'uploadFolder' => '1:/user_upload/files/',
-    ])]
+        uploadFolder: '1:/user_upload/files/',
+    )]
     protected ?FileReference $bar = null;
 }
Copied!

Deprecation: #97857 - Deprecate __inheritances operator in form configuration 

See forge#97857

Description 

The custom __inheritances operator, which was available only in YAML configuration files of EXT:form, has been deprecated.

Previously, this operator was used within form definition files to inherit and reuse configuration parts between form element definitions. With native YAML functionality now providing equivalent and more flexible features, this TYPO3-specific operator is no longer necessary.

Developers are encouraged to migrate to standard YAML features such as anchors, aliases, and overrides to avoid code duplication and to simplify form configuration maintenance.

Impact 

Using the __inheritances operator inside a custom YAML form configuration in EXT:form will trigger a PHP E_USER_DEPRECATED error.

Affected installations 

All installations with custom form definitions or form element configurations that use the __inheritances operator in their EXT:form YAML files are affected and need to update those files accordingly.

Migration 

The custom TYPO3 implementation using __inheritances can be replaced with standard YAML syntax.

Developers can achieve the same result by using anchors ( &), aliases ( *), and overrides ( <<:).

Before:

mixins:
  formElementMixins:
    BaseFormElementMixin:
      1761226183:
        identifier: custom
        templateName: Inspector-TextEditor
        label: Custom editor
        propertyPath: custom
    OtherBaseFormElementMixin:
      1761226184:
        identifier: otherCustom
        templateName: Inspector-TextEditor
        label: Other custom editor
        propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            __inheritances:
              10: 'mixins.formElementMixins.BaseFormElementMixin'
              20: 'mixins.formElementMixins.OtherBaseFormElementMixin'
Copied!

After:

customEditor: &customEditor
  1761226183:
    identifier: custom
    templateName: Inspector-TextEditor
    label: Custom editor
    propertyPath: custom

otherCustomEditor: &otherCustomEditor
  identifier: otherCustom
  templateName: Inspector-TextEditor
  label: Other custom editor
  propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            <<: *customEditor
            1761226184: *otherCustomEditor
Copied!

Deprecation: #98453 - Scheduler task registration via SC_OPTIONS 

See forge#98453

Description 

The registration of scheduler tasks via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] has been deprecated in favor of the new native scheduler task feature using TCA.

Previously, scheduler tasks were registered in ext_localconf.php using the following syntax:

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

This approach required a separate \AdditionalFieldProviderInterface implementation to handle custom task fields. The AdditionalFieldProvider was responsible for:

  • Rendering form fields in the scheduler module.
  • Validating field input.
  • Saving and loading field values.

The new approach replaces this with native TCA configuration, providing:

  • Better integration with TYPO3's FormEngine.
  • Automatic validation through TCA field configuration.
  • Enhanced security through FormEngine's XSS protection.
  • Consistency with other TYPO3 backend forms.
  • Access to all TCA field types and rendering options.

In addition, the class \AbstractAdditionalFieldProvider and the interface \AdditionalFieldProviderInterface have been deprecated.

Impact 

Using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] for registering scheduler tasks will stop working in TYPO3 v15.0.

Custom task classes implementing \AdditionalFieldProviderInterface should remove this interface implementation. The interface methods ( getAdditionalFields(), validateAdditionalFields(), saveAdditionalFields()) are no longer needed with the new TCA-based approach.

Affected installations 

All installations with custom scheduler tasks registered via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] and using \AdditionalFieldProviderInterface.

Migration 

Scheduler tasks should now be registered as native task types using TCA. This provides a more integrated and maintainable approach to task configuration.

Migration steps:

  1. Remove the registration from ext_localconf.php.
  2. Create a TCA override file in Configuration/TCA/Overrides/scheduler_my_task_type.php.
  3. Update your task class to implement the new parameter methods.
  4. Remove the \AdditionalFieldProvider class if it exists.

Example migration 

Before:

ext_localconf.php
use MyVendor\MyExtension\Task\MyTask;
use MyVendor\MyExtension\Task\MyTaskAdditionalFieldProvider;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

After:

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

defined('TYPO3') or die();

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    // Add custom fields to the tx_scheduler_task table
    ExtensionManagementUtility::addTCAcolumns(
        'tx_scheduler_task',
        [
            'my_extension_field' => [
                'label' => 'my_extension.messages:field.label',
                'config' => [
                    'type' => 'input',
                    'size' => 30,
                    'required' => true,
                    'eval' => 'trim', // FormEngine validation replaces custom validation
                    'placeholder' => 'Enter value here...',
                ],
            ],
            'my_extension_email_list' => [
                'label' => 'my_extension.messages:emailList.label',
                'config' => [
                    'type' => 'text',
                    'rows' => 3,
                    'required' => true, // 'required' validation handled by FormEngine
                    'placeholder' => 'admin@example.com',
                ],
            ],
        ]
    );

    // Register the task type
    ExtensionManagementUtility::addRecordType(
        [
            'label' => 'my_extension.messages:my_task.title',
            'description' => 'my_extension.messages:my_task.description',
            'value' => MyTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'group' => 'my_extension',
        ],
        '
        --div--;core.tabs:general,
            tasktype,
            task_group,
            description,
            my_extension_field,
            my_extension_email_list,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            --palette--;;execution,
        --div--;core.tabs:access,
            disable,
        --div--;core.tabs:extended,',
        [],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Update your (existing) task class to implement the new methods:

EXT:my_extension/Classes/Task/MyTask.php
namespace MyVendor\MyExtension\Task;

use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class MyTask extends AbstractTask
{
    protected string $myField = '';
    protected string $emailList = '';

    public function execute(): bool
    {
        // Your task logic here using $this->myField and $this->emailList
        return true;
    }

    /**
     * Return current field values as an associative array.
     * This method is called during migration from old serialized tasks
     * and when displaying task information.
     */
    public function getTaskParameters(): array
    {
        return [
            'my_extension_field' => $this->myField,
            'my_extension_email_list' => $this->emailList,
        ];
    }

    /**
     * Set field values from an associative array.
     * This method handles both old and new parameter formats for migration.
     *
     * @param array $parameters Values from either old AdditionalFieldProvider or new TCA fields.
     */
    public function setTaskParameters(array $parameters): void
    {
        // Handle migration: check old parameter names first, then new TCA field names
        $this->myField = $parameters['myField'] ?? $parameters['my_extension_field'] ?? '';
        $this->emailList = $parameters['emailList'] ?? $parameters['my_extension_email_list'] ?? '';
    }

    /**
     * Validate task parameters.
     * Only implement this method for validation that cannot be handled by FormEngine.
     * Basic validation like 'required' should be done via TCA 'eval' configuration.
     */
    public function validateTaskParameters(array $parameters): bool
    {
        $isValid = true;

        // Example: Custom email validation (beyond basic 'required' check)
        $emailList = $parameters['my_extension_email_list'] ?? '';
        if (!empty($emailList)) {
            $emails = GeneralUtility::trimExplode(',', $emailList, true);
            foreach ($emails as $email) {
                if (!GeneralUtility::validEmail($email)) {
                    GeneralUtility::makeInstance(FlashMessageService::class)
                        ->getMessageQueueByIdentifier()
                        ->addMessage(
                            GeneralUtility::makeInstance(
                                FlashMessage::class,
                                'Invalid email address: ' . $email,
                                '',
                                ContextualFeedbackSeverity::ERROR
                            )
                        );
                    $isValid = false;
                }
            }
        }

        return $isValid;
    }

    public function getAdditionalInformation(): string
    {
        $info = [];
        if ($this->myField !== '') {
            $info[] = 'Field: ' . $this->myField;
        }
        if ($this->emailList !== '') {
            $info[] = 'Emails: ' . $this->emailList;
        }
        return implode(', ', $info);
    }
}
Copied!

Key methods explained 

The new TCA-based approach uses three key methods for parameter handling:

getTaskParameters(): array

This method is already implemented in AbstractTask to handle task class properties automatically, but it can be overridden in task classes for custom behavior.

The method is primarily used:

  • For migration from the old serialized task format to the new TCA structure.
  • For non-native (deprecated) task types to store their values in the legacy parameters field.

For native TCA tasks, this method is typically no longer needed in custom tasks after the migration has been done, since field values are then stored directly in database columns.

setTaskParameters(array $parameters): void

Sets field values from an associative array. This method handles:

  • Migration from old \AdditionalFieldProvider field names to new TCA field names.
  • Loading saved task configurations when editing or executing tasks.
  • Parameter mapping during task creation and updates.
  • The method should always be implemented, especially for native tasks.

The migration pattern is: $this->myField = $parameters['oldName'] ?? $parameters['new_tca_field_name'] ?? '';

validateTaskParameters(array $parameters): bool

Optional method. Only implement this for validation that cannot be handled by FormEngine.

  • Basic validation (required, trim, etc.) should be done via TCA configuration (required property and eval options).
  • Use this method for complex business logic validation (e.g., email format validation, external API checks).
  • Return false and add a FlashMessage for validation errors.
  • FormEngine automatically handles standard TCA validation rules.

For a complete working example, see SystemStatusUpdateTask and its corresponding TCA configuration in EXT:reports/Configuration/TCA/Overrides/scheduler_system_status_update_task.php.

Deprecation: #106393 - Various methods in BackendUtility 

See forge#106393

Description 

Due to the introduction of the Schema API, several methods of \TYPO3\CMS\Backend\Utility\BackendUtility that retrieve information from $GLOBALS['TCA'] have been deprecated:

  • BackendUtility::getCommonSelectFields()
  • BackendUtility::getItemLabel()
  • BackendUtility::isTableLocalizable()
  • BackendUtility::isTableWorkspaceEnabled()
  • BackendUtility::isRootLevelRestrictionIgnored()
  • BackendUtility::isWebMountRestrictionIgnored()
  • BackendUtility::resolveFileReferences()

Impact 

Calling any of the mentioned methods now triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that directly call these methods are affected.

Migration 

The migration strategy is the same for all cases: use the corresponding Schema API methods directly in your code. In most cases, you'll need to inject TcaSchemaFactory via dependency injection.

getCommonSelectFields 

No substitution is available. The method was marked as @internal already. If your code depends on this functionality, copy the method into your own extension.

getItemLabel 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::getItemLabel('pages', 'title');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
$schema = $this->schemaFactory->has('pages')
    ? $this->schemaFactory->get('pages')
    : null;
return $schema !== null && $schema->hasField('title')
    ? $schema->getField('title')->getLabel()
    : null;
Copied!

isTableLocalizable 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableLocalizable('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Language);
Copied!

isTableWorkspaceEnabled 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableWorkspaceEnabled('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Workspace);
Copied!

isRootLevelRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isRootLevelRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->getCapability(
            TcaSchemaCapability::RestrictionRootLevel
        )->shallIgnoreRootLevelRestriction();
Copied!

isWebMountRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isWebMountRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::RestrictionWebMount);
Copied!

resolveFileReferences 

No substitution is available. Copy the method into your own codebase and adapt it as needed.

Deprecation: #106405 - AbstractTypolinkBuilder->build 

See forge#106405

Description 

The build() method in \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder has been deprecated in favor of the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

When creating custom TypolinkBuilder classes, the traditional approach was to:

  1. Extend \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder
  2. Implement the build() method
  3. Receive dependencies via the constructor

This approach is now deprecated. The new, recommended pattern is to implement TypolinkBuilderInterface directly.

The ContentObjectRenderer property in AbstractTypolinkBuilder has also been deprecated and will be removed in TYPO3 v15.0. It should now be accessed via the \Psr\Http\Message\ServerRequestInterface object instead.

Impact 

Extension authors who have created custom TypolinkBuilder classes that extend AbstractTypolinkBuilder will see deprecation warnings when their link builders are used.

Deprecation warnings occur when:

  • A custom TypolinkBuilder class still implements the build() method
  • Code accesses the deprecated $contentObjectRenderer property
  • Dependencies are passed through the constructor instead of DI

Affected installations 

TYPO3 installations with extensions that:

  • Create custom TypolinkBuilder classes extending AbstractTypolinkBuilder
  • Override or extend the build() method
  • Access the $contentObjectRenderer property directly

Migration 

For end users, link generation continues to work as before using either:

  • ContentObjectRenderer , method typolink()
  • LinkFactory

These public APIs are not affected and do not trigger deprecations.

For extension developers, custom TypolinkBuilder implementations should now use the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

  1. Implement the new interface:
Recommended migration approach
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

// Before (deprecated)
class MyCustomLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(
        array &$linkDetails,
        string $linkText,
        string $target,
        array $conf
    ): LinkResultInterface {
        // Custom logic using $this->contentObjectRenderer
    }
}

// After (recommended)
class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = ''
    ): LinkResultInterface {
        $contentObjectRenderer = $request->getAttribute(
            'currentContentObject'
        );
        // Custom logic using $contentObjectRenderer
    }
}
Copied!
  1. Use dependency injection for required services instead of accessing them through global state or constructor arguments.
  2. Retrieve ContentObjectRenderer from the request object rather than the deprecated property.

All implementations of TypolinkBuilderInterface are automatically registered as public services in the Dependency Injection container, so no manual service configuration is necessary.

Extensions can maintain backward compatibility during the transition by implementing both the old build() and the new buildLink() methods.

Deprecation: #106527 - markFieldAsChanged() moved to FormEngine main module 

See forge#106527

Description 

The static method markFieldAsChanged() in the module @typo3/backend/form-engine-validation is used to modify the markup in the DOM to mark a field as changed. Technically, this is unrelated to validation itself, therefore the method has been moved to @typo3/backend/form-engine.

Impact 

Calling markFieldAsChanged() from @typo3/backend/form-engine-validation will trigger a deprecation notice in the browser console.

Affected installations 

All extensions using the deprecated code are affected.

Migration 

If not already done, import the main FormEngine module and call markFieldAsChanged() from there.

Example:

- import FormEngineValidation from '@typo3/backend/form-engine-validation.js';
+ import FormEngine from '@typo3/backend/form-engine.js';

- FormEngineValidation.markFieldAsChanged(fieldReference);
+ FormEngine.markFieldAsChanged(fieldReference);
Copied!

Example compatibility layer:

import FormEngine from '@typo3/backend/form-engine.js';
import FormEngineValidation from '@typo3/backend/form-engine-validation.js';

if (typeof FormEngine.markFieldAsChanged === 'function') {
  FormEngine.markFieldAsChanged(fieldReference);
} else {
  FormEngineValidation.markFieldAsChanged(fieldReference);
}
Copied!

Deprecation: #106618 - GeneralUtility::resolveBackPath 

See forge#106618

Description 

The method \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath has been marked as deprecated and will be removed in TYPO3 v15.0.

It served as a mechanism to remove relative path segments such as ".." when referencing files or directories. This was particularly important before TYPO3 v7, when every TYPO3 backend module had its own route and entry point PHP file. Nowadays, it is a relic from the past.

Since TYPO3 v13, this method has become even less relevant, as both the TYPO3 backend and frontend now share the same main entry point file (index.php).

Impact 

TYPO3 no longer resolves the back path of resource references or normalizes paths when rendering or referencing resources in the HTML output - neither in the frontend nor in the backend.

However, existing references will continue to work.

Affected installations 

TYPO3 installations with custom TypoScript inclusions or backend modules that reference files using relative paths may be affected. Such usage is uncommon in modern TYPO3 installations.

Migration 

References to resources should now use the EXT: prefix or be written relative to the public web path of the TYPO3 installation.

References to JavaScript modules (ES6 modules) should be managed through import maps using module names instead of relative paths.

Deprecation: #106821 - Workspace aware inline child tables are enforced 

See forge#106821

Description 

TCA tables that are used as inline child tables in a standard foreign_table relationship must be declared as workspace aware if their parent table is workspace aware.

Impact 

The TYPO3 Core may automatically add versioningWS = true to the ctrl section of inline child tables. In this case, a deprecation-level log entry will be issued, indicating that the TCA definition should be updated accordingly.

This TCA definition should be added even if the workspace system extension is not loaded. The database schema analyzer will then automatically add the required workspace-related database columns.

Affected installations 

TYPO3 instances with extensions that define inline relations where the parent table is workspace aware but the child table is not are affected.

A typical scenario involves inline child tables attached to the tt_content table.

Migration 

An automatic TCA migration scans TCA configurations for this scenario and adds versioningWS = true to affected child tables. Developers should add this declaration manually to their TCA to satisfy the migration and suppress deprecation log messages.

'ctrl' => [
    'versioningWS' => true,
],
Copied!

Note that the automatic migration does not detect children attached to inline fields within type => 'flex' (flex form) fields. Developers and integrators should still explicitly declare such child tables as workspace aware.

In general, the combination of "parent is workspace aware" and "child is not workspace aware" is not supported by the TYPO3 Core in inline foreign_table setups—regardless of whether the parent field is a database column or part of a flex form.

Deprecation: #106969 - Deprecate User TSConfig auth.BE.redirectToURL 

See forge#106969

Description 

The User TSConfig option auth.BE.redirectToURL has been marked as deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated User TSConfig option triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the User TSConfig option auth.BE.redirectToURL are affected.

Migration 

If a redirect after a successful backend login is required, create a custom PSR-15 middleware to handle the redirection.

Deprecation: #107047 - ExtensionManagementUtility::addPiFlexFormValue() 

See forge#107047

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() has been deprecated.

This method was historically used to assign FlexForm definitions to plugins registered as subtypes in the list_type field of tt_content. With the removal of subtypes (see Breaking: #105377 - Deprecated functionality removed) and the shift toward registering plugins as dedicated record types via CType, as well as the removal of the ds_pointerField and multi-entry ds array format (see Breaking: #107047 - Remove pointer field functionality of TCA flex), this separate method call is no longer necessary.

Impact 

Calling this method will trigger a deprecation warning. The extension scanner will also report any usages. The method will be removed in TYPO3 v15.0.

Affected installations 

Extensions that call \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() to assign FlexForm definitions to plugins or content elements are affected.

Migration 

Instead of using this method, define the FlexForm configuration using one of the following approaches:

Option 1: Register via registerPlugin() 

Provide the FlexForm definition directly when registering the plugin. This has been possible since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'My Plugin Title',
    'my-extension-icon',
    'plugins',
    'Plugin description',
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 2: Register via addPlugin() for non-Extbase plugins 

This is also supported since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\SelectItemUtility\SelectItem;

ExtensionManagementUtility::addPlugin(
    new SelectItem(
        'select',
        'My Plugin Title',
        'myextension_myplugin',
        'my-extension-icon',
        'plugins',
        'Plugin description'
    ),
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 3: Define FlexForm via TCA columnsOverrides 

You can also directly define the FlexForm in TCA:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['types']['my_plugin']['columnsOverrides']
    ['pi_flexform']['config']['ds'] =
    'FILE:EXT:my_extension/Configuration/FlexForm.xml';
Copied!

Deprecation: #107057 - Deprecate auto-render assets sections 

See forge#107057

Description 

The auto-rendering of template sections HeaderAssets and FooterAssets available in Fluid templates has been marked as deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated sections will raise a deprecation-level log error and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the sections HeaderAssets and FooterAssets in Fluid templates.

Migration 

It is recommended to use the f:asset.script or f:asset.css ViewHelpers from the TYPO3 Asset Collector API to render required assets.

In scenarios where the f:asset.script or f:asset.css ViewHelpers are not suitable, users can use the f:page.headerData or f:page.footerData ViewHelpers to render custom HTML header or footer markup.

Deprecation: #107225 - Boolean sort direction in FileList->start() 

See forge#107225

Description 

The fourth parameter of the method \TYPO3\CMS\Filelist\FileList::start() has been renamed from $sortRev to $sortDirection and now accepts both boolean values (for backward compatibility) and SortDirection enum values.

Passing a boolean value for the sort direction (fourth parameter of FileList::start()) has been deprecated in favor of the new \TYPO3\CMS\Filelist\Type\SortDirection enum, providing better type safety and clarity. The parameter name has also changed from $sortRev to $sortDirection to more accurately describe its purpose.

Impact 

Calling FileList::start() with a boolean value as the fourth parameter triggers a deprecation warning. The functionality will continue to work for now but will be removed in TYPO3 v15.0.

Affected installations 

All installations using FileList::start() directly with a boolean value for the sort direction parameter are affected. This mainly applies to custom file browser implementations or extensions that instantiate and configure the FileList class directly, even though it is marked as @internal.

Migration 

Replace boolean values with the corresponding SortDirection enum values:

Example migration
use TYPO3\CMS\Filelist\Type\SortDirection;

// Before (deprecated)
$fileList->start($folder, $currentPage, $sortField, false, $mode);
$fileList->start($folder, $currentPage, $sortField, true, $mode);

// After
$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::ASCENDING,
    $mode
);

$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::DESCENDING,
    $mode
);
Copied!

The migration maintains the same functionality:

  • true (descending) → SortDirection::DESCENDING
  • false (ascending) → SortDirection::ASCENDING

Deprecation: #107229 - Deprecate Annotation namespace of Extbase attributes 

See forge#107229

Description 

With the removed support for PHP annotations in Extbase, the class namespace \TYPO3\CMS\Extbase\Annotation no longer reflects classes using PHP annotations. It now contains PHP attributes only.

The class namespace of all PHP attributes used with models, DTOs, and controller actions in the Extbase context has been moved to \TYPO3\CMS\Extbase\Attribute.

Developers and integrators are advised to migrate existing class usages to the new namespace. The deprecated namespace will be removed in TYPO3 v15.0.

Impact 

Usage of the deprecated class namespace is no longer recommended but will continue to work until TYPO3 v15.0.

Affected installations 

Instances using the deprecated class namespace for Extbase attributes in models, DTOs, or controller actions are affected.

Migration 

Aside from migrating PHP annotations to PHP attributes, existing usages of the class namespace \TYPO3\CMS\Extbase\Annotation should be updated to \TYPO3\CMS\Extbase\Attribute:

-TYPO3\CMS\Extbase\Annotation
+TYPO3\CMS\Extbase\Attribute
Copied!

Before: 

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

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

After: 

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

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

Deprecation: #107287 - FileCollectionRegistry->addTypeToTCA() method 

See forge#107287

Description 

The method \TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry->addTypeToTCA() has been deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

This method was originally intended to register additional file collection types by directly manipulating the global $GLOBALS['TCA'] array for the sys_file_collection table. With modern TCA configuration patterns, this approach is no longer recommended.

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances using the FileCollectionRegistry->addTypeToTCA() method directly are affected.

The extension scanner will report usages as weak match.

Migration 

Instead of using this method, configure file collection types directly in your TCA configuration files. Move the TCA configuration from the method call to your extension's Configuration/TCA/Overrides/sys_file_collection.php file.

Before (deprecated)
use TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$fileCollectionRegistry = GeneralUtility::makeInstance(
    FileCollectionRegistry::class
);
$fileCollectionRegistry->addTypeToTCA(
    'mytype',
    'My Collection Type',
    'description,my_field',
    ['my_field' => ['config' => ['type' => 'input']]]
);
Copied!
After (recommended) – EXT:my_extension/Configuration/TCA/Overrides/sys_file_collection.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

$GLOBALS['TCA']['sys_file_collection']['types']['mytype'] = [
    'showitem' => '
        sys_language_uid, l10n_parent, l10n_diffsource,
        title, --palette--;;1, type, description, my_field
    ',
];

$GLOBALS['TCA']['sys_file_collection']['columns']['type']['config']['items'][] =
    [
        'label' => 'My Collection Type',
        'value' => 'mytype',
    ];

// Add additional columns if needed
ExtensionManagementUtility::addTCAcolumns(
    'sys_file_collection',
    [
        'my_field' => [
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);
Copied!

Deprecation: #107413 - PathUtility getRelativePath(to) methods 

See forge#107413

Description 

The following methods in \TYPO3\CMS\Core\Utility\PathUtility have been marked as deprecated and will be removed in TYPO3 v15.0:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

These methods are no longer needed, as TYPO3's path handling has been simplified due to the unification of the entry point URLs.

Since TYPO3 v13, both frontend and backend use the same main entry point (htdocs/index.php), making relative path calculations obsolete.

Impact 

Calling these methods will trigger a PHP deprecation warning. They will continue to work as before until they are removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call the deprecated methods are affected:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

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

Migration 

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

  • Use GeneralUtility::getFileAbsFileName() for extension resources
  • Use the EXT: prefix when referencing extension resources
  • Use PathUtility::getPublicResourceWebPath() for public extension resources
  • Reference paths relative to the public web path of the TYPO3 installation
Before (deprecated)
use TYPO3\CMS\Core\Utility\PathUtility;

$relativePath = PathUtility::getRelativePath($sourcePath, $targetPath);
$relativeToPath = PathUtility::getRelativePathTo($absolutePath);
Copied!
After (recommended)
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

// Use absolute paths or appropriate TYPO3 APIs
$absolutePath = GeneralUtility::getFileAbsFileName(
    'EXT:my_extension/Resources/Public/file.js'
);

$webPath = PathUtility::getPublicResourceWebPath(
    'EXT:my_extension/Resources/Public/file.js'
);
Copied!

Deprecation: #107436 - Localization Parsers 

See forge#107436

Description 

Due to the migration to Symfony Translation components (see Feature: #107436 - Symfony Translation Component integration), the following localization parser classes have been deprecated:

  • \TYPO3\CMS\Core\Localization\Parser\AbstractXmlParser
  • \TYPO3\CMS\Core\Localization\Parser\LocalizationParserInterface
  • \TYPO3\CMS\Core\Localization\Parser\XliffParser

The global configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] has been removed and replaced with $GLOBALS['TYPO3_CONF_VARS']['LANG']['loader'] .

Impact 

Using any of the mentioned parser classes will raise a deprecation level log entry and will stop working in TYPO3 v15.0.

The extension scanner will report usages as strong match.

Affected installations 

Instances using the deprecated localization parser classes directly or configuring custom parsers via the removed global configuration option.

Migration 

Replace usage of the deprecated parser classes with Symfony Translation loaders. Use the new XliffLoader class for XLIFF file processing.

The Symfony Translator and its loaders are now responsible for file parsing and should be used instead of the deprecated TYPO3 parsers.

For custom localization needs, implement Symfony Translation loader interfaces instead of the deprecated TYPO3 parser interfaces.

Configuration changes:

use TYPO3\CMS\Core\Localization\Loader\XliffLoader;
use TYPO3\CMS\Core\Localization\Parser\XliffParser;

// Before
$GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser']['xlf'] = XliffParser::class;

// After
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['xlf'] = XliffLoader::class;
Copied!

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Deprecation: #107537 - GeneralUtility::createVersionNumberedFilename 

See forge#107537

Description 

GeneralUtility::createVersionNumberedFilename adds cache busting to a file URL, when called in a certain and correct order with other legacy API methods to create URLs from system resources.

This class and its functionality is superseded by the System Resource API.

Impact 

Calling this method will trigger a PHP deprecation warning. The method will continue to work as is, until it is removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call this deprecated method:

  • GeneralUtility::createVersionNumberedFilename

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $file): string
{
    $file = GeneralUtility::getFileAbsFileName($file);
    $partialUrl = GeneralUtility::createVersionNumberedFilename($file);
    return PathUtility::getAbsoluteWebPath($partialUrl);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - FilePathSanitizer service 

See forge#107537

Description 

\TYPO3\CMS\Frontend\Resource\FilePathSanitizer was introduced as a service class to replace the logic that was previously part of $TSFE->tmpl.

The goal of this class was to validate and manipulate given strings to be used for URL generation later in the process.

This class and its functionality are superseded by the System Resource API.

Impact 

All installations using the \FilePathSanitizer will trigger a deprecation notice when the class is instantiated.

Otherwise, this class will continue to work as is, until removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that instantiate \FilePathSanitizer.

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

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly FilePathSanitizer $pathSanitizer,
) {}

public function renderUrl(string $someString): string
{
    $pathRelativeToPublicDir = $this->pathSanitizer->sanitize($someString);
    return $this->codeThatDoesDetectTheCorrectUrlPrefix()
        . $pathRelativeToPublicDir;
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - TYPO3CMSCoreUtilityPathUtility::getPublicResourceWebPath 

See forge#107537

Description 

The static method \TYPO3\CMS\Core\Utility\PathUtility::getPublicResourceWebPath($extResource) was marked internal since its introduction. Since there were no good alternatives to this API, it is now deprecated first, before being removed with TYPO3 v15.0.

Impact and affected installations 

TYPO3 installations using PathUtility::getPublicResourceWebPath($extResource) will receive a deprecation message for each call of this method.

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $extResource): string
{
    return PathUtility::getPublicResourceWebPath($extResource);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107550 - Table Garbage Collection Task configuration via $GLOBALS 

See forge#107550

Description 

The \TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']

has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the TableGarbageCollectionTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

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

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']
    [TableGarbageCollectionTask::class]['options']['tables']
    ['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'expirePeriod' => 90,
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_table_garbage_collection.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'expirePeriod' => 90,
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the expirePeriod of table sys_log:

use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['sys_log']['expirePeriod'] = 240;
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107562 - IP Anonymization Task configuration via $GLOBALS 

See forge#107562

Description 

The \TYPO3\CMS\Scheduler\Task\IpAnonymizationTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'] has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the IpAnonymizationTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

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

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables']['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'ipField' => 'private_ip',
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_ip_anonymization.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables'] ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'ipField' => 'private_ip',
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the dateField of sys_log:

use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables']['sys_log']['dateField']
        = 'custom_date';
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107648 - InfoboxViewHelper STATE_* constants 

See forge#107648

Description 

The public constants in \TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper for defining the state or severity of an infobox have been deprecated:

  • InfoboxViewHelper::STATE_NOTICE
  • InfoboxViewHelper::STATE_INFO
  • InfoboxViewHelper::STATE_OK
  • InfoboxViewHelper::STATE_WARNING
  • InfoboxViewHelper::STATE_ERROR

These constants have been superseded by the dedicated enum \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity , which provides a single source of truth for severity levels across the entire TYPO3 Core and improves type safety and maintainability.

Impact 

Using these constants will trigger a PHP deprecation warning. The constants will be removed in TYPO3 v15.0. The extension scanner will report usages as weak match.

Affected installations 

Instances using any of the STATE_* constants from InfoboxViewHelper in their PHP code or Fluid templates.

Migration 

Replace the deprecated constants with the corresponding ContextualFeedbackSeverity enum.

Example (PHP)
// Before
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
$state = InfoboxViewHelper::STATE_ERROR;

// After - Recommended: Use the enum directly
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
$severity = ContextualFeedbackSeverity::ERROR;

// Alternative: Use the integer value when explicitly needed
$stateValue = ContextualFeedbackSeverity::ERROR->value;
Copied!

In Fluid templates, use the enum via f:constant():

Example (Fluid Template)
<!-- Before -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper::STATE_ERROR')}">
    Error message
</f:be.infobox>

<!-- After -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR')}">
    Error message
</f:be.infobox>
Copied!

The InfoboxViewHelper has been updated to accept both the enum directly and integer values for backward compatibility.

Mapping table 

Deprecated constant Replacement Value
InfoboxViewHelper::STATE_NOTICE ContextualFeedbackSeverity::NOTICE->value -2
InfoboxViewHelper::STATE_INFO ContextualFeedbackSeverity::INFO->value -1
InfoboxViewHelper::STATE_OK ContextualFeedbackSeverity::OK->value 0
InfoboxViewHelper::STATE_WARNING ContextualFeedbackSeverity::WARNING->value 1
InfoboxViewHelper::STATE_ERROR ContextualFeedbackSeverity::ERROR->value 2

Deprecation: #107725 - Deprecate usage of array in password for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password.

With this change, TYPO3's Redis cache backend supports username and password authentication directly. You can now configure the TYPO3 Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The password configuration option of the Redis cache backend is now typed as array|string.

Setting this configuration option with an array is deprecated and will be removed in TYPO3 v15.0.

Affected installations 

All installations using the Redis cache backend and configuring the password option as an array containing both username and password values are affected.

Migration 

Use the dedicated configuration options username and password instead of passing an array to password.

Before (deprecated):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'password' => [
            'user' => 'redis',
            'pass' => 'redis',
        ],
    ];
Copied!

After (recommended):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Deprecation: #107813 - Deprecate MetaInformation API in DocHeader 

See forge#107813

Description 

The \TYPO3\CMS\Backend\Template\Components\MetaInformation class and related methods in \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent have been deprecated in favor of the new breadcrumb component architecture.

The following have been marked as deprecated:

  • DocHeaderComponent::setMetaInformation()
  • DocHeaderComponent::setMetaInformationForResource()
  • MetaInformation class

These APIs were previously used to display page navigation paths in the backend document header. This functionality is now handled by the breadcrumb component, which provides richer context and better navigation capabilities.

Impact 

Calling any of the deprecated methods will trigger a PHP E_USER_DEPRECATED error.

The \MetaInformation class is now marked as @internal and should not be used in extensions.

Affected installations 

All installations using custom backend modules that call:

  • $view->getDocHeaderComponent()->setMetaInformation($pageRecord)
  • $view->getDocHeaderComponent()->setMetaInformationForResource($resource)

or any custom code relying on the \MetaInformation class.

The Extension Scanner will detect usage of the deprecated methods and classes, making it easy to identify code that needs to be updated.

Migration 

Replace calls to the deprecated methods with the new convenience methods on DocHeaderComponent .

Before:

Example (before)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setMetaInformation($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setMetaInformationForResource($resource);
Copied!

After:

Example (after)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setResourceBreadcrumb($resource);
Copied!

An additional convenience method is available for records:

Example (record breadcrumb)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For any record type (by table and UID)
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);
Copied!

For advanced scenarios requiring custom breadcrumb logic (such as conditional context selection based on controller state), see the implementation in \TYPO3\CMS\Backend\Controller\EditDocumentController , which uses \TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory directly with setBreadcrumbContext().

Deprecation: #107823 - ButtonBar, Menu, and MenuRegistry make* methods deprecated 

See forge#107823

Description 

The factory methods in \TYPO3\CMS\Backend\Template\Components\ButtonBar for creating button instances, in \TYPO3\CMS\Backend\Template\Components\Menu for creating menu item instances, and in \TYPO3\CMS\Backend\Template\Components\MenuRegistry for creating menu instances have been deprecated in favor of using the new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class directly.

The following methods are now deprecated:

  • ButtonBar::makeGenericButton()
  • ButtonBar::makeInputButton()
  • ButtonBar::makeSplitButton()
  • ButtonBar::makeDropDownButton()
  • ButtonBar::makeLinkButton()
  • ButtonBar::makeFullyRenderedButton()
  • ButtonBar::makeShortcutButton()
  • ButtonBar::makeButton()
  • Menu::makeMenuItem()
  • MenuRegistry::makeMenu()

Impact 

Calling any of the deprecated make*() methods on ButtonBar , \Menu, or MenuRegistry will trigger a PHP deprecation notice.

The methods continue to work in TYPO3 v14 but will be removed in TYPO3 v15.

Affected installations 

All extensions using ButtonBar::make*() methods to create buttons, Menu::makeMenuItem() to create menu items, or MenuRegistry::makeMenu() to create menus are affected. The extension scanner will report any usages.

Migration 

Inject \TYPO3\CMS\Backend\Template\Components\ComponentFactory in your controller and use its create*() methods instead of ButtonBar::make*().

Before:

Example (before)
use Psr\Http\Message\ResponseInterface;

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $buttonBar->makeLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

After:

Example (after)
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $this->componentFactory->createLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

Additionally, consider using the preconfigured button creation methods like createBackButton(), createCloseButton(), createSaveButton(), createReloadButton(), and createViewButton() for common button patterns.

For the low-level makeButton(string $className) method, use GeneralUtility::makeInstance() directly or the appropriate ComponentFactory::create*() method:

Example (button instantiation)
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before:
$button = $buttonBar->makeButton(MyCustomButton::class);

// After (option 1 - direct instantiation):
$button = GeneralUtility::makeInstance(MyCustomButton::class);

// After (option 2 - via factory if it's a standard button):
$button = $this->componentFactory->createLinkButton();
Copied!

For Menu::makeMenuItem(), use ComponentFactory::createMenuItem():

Example (menu items)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menu = $menuRegistry->makeMenu();
$menuItem = $menu->makeMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menu = $this->componentFactory->createMenu();
$menuItem = $this->componentFactory->createMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);
Copied!

For MenuRegistry::makeMenu(), use ComponentFactory::createMenu():

Example (menus)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $menuRegistry->makeMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $this->componentFactory->createMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');
Copied!

Additionally, note that Menu::addMenuItem() now returns static to support fluent interface patterns:

Example (fluent chaining)
// Now possible with fluent interface:
$menu->addMenuItem($menuItem1)
    ->addMenuItem($menuItem2)
    ->addMenuItem($menuItem3);
Copied!

Deprecation: #107938 - Deprecate unused XLIFF files 

See forge#107938

Description 

The following XLIFF files have been deprecated, as they are not used in the TYPO3 Core anymore:

  • EXT:backend/Resources/Private/Language/locallang_view_help.xlf
  • EXT:backend/Resources/Private/Language/locallang_sitesettings_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_mod.xlf
  • EXT:belog/Resources/Private/Language/locallang_mod.xlf
  • EXT:beuser/Resources/Private/Language/locallang_mod.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_usertools.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_system.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_site.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_file.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_help.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_admintools.xlf
  • EXT:core/Resources/Private/Language/locallang_tsfe.xlf
  • EXT:dashboard/Resources/Private/Language/locallang_mod.xlf
  • EXT:extensionmanager/Resources/Private/Language/locallang_mod.xlf
  • EXT:form/Resources/Private/Language/locallang_module.xlf
  • EXT:indexed_search/Resources/Private/Language/locallang_mod.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallUpgrade.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallSettings.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallMaintenance.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallEnvironment.xlf
  • EXT:install/Resources/Private/Language/BackendModule.xlf
  • EXT:info/Resources/Private/Language/locallang_mod_web_info.xlf
  • EXT:linkvalidator/Resources/Private/Language/Module/locallang_mod.xlf
  • EXT:recycler/Resources/Private/Language/locallang_mod.xlf

They will be removed with TYPO3 v15.0.

The console command vendor/bin/typo3 language:domain:list does not list deprecated language domains, unless the option --deprecated is used.

Impact 

Using a label reference from one of these files triggers a E_USER_DEPRECATED error.

Affected installations 

Third-party extensions and site packages that use labels from the listed sources will not be able to display the affected labels with TYPO3 v15.0.

Migration 

If the desired string is contained in another language domain, consider using that domain. Otherwise, move the required labels into your extension or site package.

Deprecation: #107963 - sys_redirect default type name changed to "default" 

See forge#107963

Description 

The default type name for the sys_redirect table has been changed from '1' to 'default' in TYPO3 v14.0 to align with TYPO3 naming conventions and allow for better type extensibility.

Extensions that directly access $GLOBALS['TCA']['sys_redirect']['types']['1'] to manipulate the redirect TCA configuration must be updated to use the new 'default' key instead.

Impact 

Direct access to $GLOBALS['TCA']['sys_redirect']['types']['1'] will no longer work, as the type has been renamed to 'default'.

Extensions that manipulate the sys_redirect TCA type configuration must be updated accordingly.

The TCA migration layer will automatically migrate any custom $GLOBALS['TCA']['sys_redirect']['types']['1'] definitions to 'default' during TCA compilation, but a deprecation message will be logged.

Affected installations 

Instances with extensions that directly manipulate $GLOBALS['TCA']['sys_redirect']['types']['1'] to customize the default redirect record type.

Migration 

Update your TCA override files to use the new type name 'default' instead of '1'.

// Before - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['1']['label']
    = 'My custom label';

// After - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['default']['label']
    = 'My custom label';
Copied!

Deprecation: #108008 - Manual shortcut button creation 

See forge#108008

Description 

Manually creating and adding \ShortcutButton instances to the button bar is deprecated and will trigger a deprecation warning.

Controllers should use the new \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent::setShortcutContext() method instead, which automatically creates and positions the shortcut button.

Impact 

Controllers that manually create and add \ShortcutButton instances to the button bar will trigger a deprecation warning. The shortcut button will still work as expected.

Affected installations 

Installations with custom backend modules that manually create shortcut buttons.

Migration 

Replace manual shortcut button creation with the new API.

Before:

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Deprecation: #108148 - Fluid LenientArgumentProcessor 

See forge#108148

Description 

Fluid 5.0 deprecates \TYPO3Fluid\Fluid\Core\ViewHelper\LenientArgumentProcessor , which will be removed with Fluid 6.0. \TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor is now used instead.

Impact 

The impact of the switch to \TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor is documented in Breaking: #108148 - Strict Types in Fluid ViewHelpers.

Affected installations 

Installations that use the \TYPO3Fluid\Fluid\Core\ViewHelper\LenientArgumentProcessor programmatically.

Migration 

The class can be copied to the project/extension if it's still required.

Deprecation: #108227 - Usage of #[IgnoreValidation] and #[Validate] attributes for parameters at method level 

See forge#108227

Description 

Usage of the following extbase attribute properties is deprecated since TYPO3 v14.0:

  • $argumentName property of #[IgnoreValidation] attribute
  • $param property of #[Validate] attribute

Instead of using these properties, the containing attributes should be placed directly at the appropriate method parameters.

Example:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Impact 

Passing a value other than null to the mentioned attribute parameters will trigger a deprecation warning. Validation will still work as expected, but will stop working with TYPO3 v15.

Affected installations 

All installations using the #[IgnoreValidation] and #[Validate] attributes in Extbase context in combination with the mentioned attribute parameters are affected.

Migration 

Developers can easily migrate their implementations by moving parameter-related attributes next to the method parameters instead of the related method.

Before:

final class FooController extends ActionController
{
    #[IgnoreValidation(argumentName: 'something')]
    public function barAction(string $something): ResponseInterface
    {
        // Do something...
    }

    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
    public function bazAction(string $anythingNotEmpty): ResponseInterface
    {
        // Do something...
    }
}
Copied!

After:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Combined diff:

 final class FooController extends ActionController
 {
-    #[IgnoreValidation(argumentName: 'something')]
-    public function barAction(string $something): ResponseInterface
-    {
+    public function barAction(
+        #[IgnoreValidation]
+        string $something,
+    ): ResponseInterface {
         // Do something...
     }


-    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
-    public function bazAction(string $anythingNotEmpty): ResponseInterface
-    {
+    public function bazAction(
+        #[Validate(validator: 'NotEmpty')]
+        string $anythingNotEmpty,
+    ): ResponseInterface {
         // Do something...
     }
 }
Copied!

Important: #104027 - New ViewHelper argument "module" to define module context 

See forge#104027

Description 

A new optional argument module has been added to the following ViewHelpers:

  • <be:link.editRecord>
  • <be:link.newRecord>
  • <be:uri.editRecord>
  • <be:uri.newRecord>

The module argument allows integrators to explicitly define the backend module context used when opening the FormEngine to edit or create a record. When set, this module will be highlighted as active in the backend menu, providing better navigation context.

This is particularly useful in scenarios where the default context cannot be reliably inferred.

Usage Example 

Example usage of module argument
<be:link.editRecord table="tt_content" uid="{record.uid}" module="web_layout">
    Edit this content element
</be:link.editRecord>

<be:uri.newRecord table="custom_table" pid="123" module="web_list" />
Copied!

Impact 

When used, the module argument ensures a more accurate and predictable backend editing experience by controlling which module is marked as active when the FormEngine opens.

Important: #105244 - Updated default .htaccess template 

See forge#105244

Description 

When installing TYPO3 for the first time, a .htaccess file is added to the htdocs or public directory when running TYPO3 via an Apache web server.

In addition to several TYPO3-specific optimizations, this file mainly contains rules (using the "mod_rewrite" Apache module) that redirect all URL requests for non-existent files within a TYPO3 project to the main index.php entry point file.

For new installations, this file now contains updated configuration that can also be applied to existing TYPO3 setups to reflect the current default behavior.

Key changes:

  • URL requests within /_assets/ and /fileadmin/ are no longer redirected, as these directories contain resources either managed by TYPO3 or by editors.
  • The directory /_assets/ has been included since TYPO3 v12 in Composer-based installations and is now officially added.
  • The folder /uploads/ is no longer maintained by TYPO3 since v11 and is now removed from the default .htaccess configuration. This means that TYPO3 pages can now officially use the URL path /uploads.

It is recommended to apply these adjustments in existing TYPO3 installations as well, even for other web servers such as nginx or IIS, provided there is no custom usage of /_assets/ or /uploads/ (for example through a PSR-15 middleware, custom extension, or custom routing).

Apache example:

In Apache-based setups, look for this line:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|uploads/) - [L]
Copied!

and replace it with:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|_assets/) - [L]
Copied!

Important: #105310 - Create CHAR and BINARY as fixed-length columns 

See forge#105310

Description 

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with \TYPO3\CMS\Core\Schema\DefaultTcaSchema information for TCA managed tables and fields.

Fixed- and variable-length variants have been parsed in the past, but failed to flag the column as $fixed = true for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

ext_tables.sql created as (before) created as (now)
CHAR(10) VARCHAR(10) CHAR(10)
VARCHAR(10) VARCHAR(10) VARCHAR(10)
BINARY(10) VARBINARY(10) BINARY(10)
VARBINARY(10) VARBINARY(10) VARBINARY(10)

Not all relational database management systems (RDBMS) behave the same way for fixed-length columns. Implementation differences need to be respected to ensure consistent query and data behaviour across all supported database systems.

Fixed-length CHAR 

Key difference between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data. CHAR, which stands for character, is a fixed-length data type. It always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters (not just 5). Unused characters are padded with spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. Thus, storing the word apple in a VARCHAR(10) column will only occupy 5 characters.

The main difference between PostgreSQL and MySQL, MariaDB or SQLite is that PostgreSQL also returns the padded spaces for values that do not fill the full defined length (for example, apple[space][space][space][space] [space]).

In addition, these padded spaces are respected in query conditions, sorting or calculations (such as concat()). These differences make a significant impact and must be considered when using CHAR fields.

Rule of thumb for fixed-length CHAR columns

  • Use only with guaranteed fixed-length values to avoid padding.
  • For 255 or more characters, VARCHAR or TEXT must be used.

More hints for fixed-length CHAR columns

  • Ensure that stored values are fixed-length (non-space characters), for example by using hash algorithms that produce fixed-length identifiers.
  • Ensure that query statements use trim or rightPad within WHERE, HAVING or SELECT operations when values are not guaranteed to be fixed-length.

Example of differences in behaviour of fixed-length CHAR types 

The following examples illustrate how different relational database management systems handle fixed-length CHAR values, and why the behaviour must be carefully considered when storing or querying such data.

Creating a fixed-length field 
Example ext_tables.sql defining a fixed-length tt_content field
CREATE TABLE `tt_content` (
    `some_label` CHAR(10) DEFAULT '' NOT NULL,
);
Copied!
Inserting example data 

Two example rows are added below: one value fits exactly 10 characters, the other uses only 6 characters.

Adding two example rows
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionForTable('tt_content');
// adding a value with 10 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'some-label',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
// adding a value with only 6 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'label1',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
Copied!
Retrieving the records 
Get all records from table
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!
Result differences across platforms 

The returned values differ depending on the database platform.

Result rows MySQL, MariaDB or SQLite
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        'some_label' => 'label1',
    ],
];
Copied!
Result rows with PostgreSQL
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        // PostgreSQL applies the fixed length to the value directly,
        // filling it up with spaces
        'some_label' => 'label1    ',
    ],
];
Copied!
Result rows difference between database platforms (commented)
 <?php

 $rows = [
     [
         'uid' => 1,
         'some_label' => 'some-label',
     ],
     [
         'uid' => 2,
-        'some_label' => 'label1',      // MySQL, MariaDB, SQLite
+        'some_label' => 'label1    ',  // PostgreSQL
     ],
 ];
Copied!
Querying trimmed versus padded values 

Using a trimmed value in a WHERE clause can match the row, but the returned value will differ depending on the database platform.

Retrieve with trimmed value
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'), // trimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1    ']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
Copied!
Enforcing trimmed values in queries 
Retrieve with enforced trimmed value.
<?php

use Doctrine\DBAL\Platforms\TrimMode;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->as(
            $queryBuilder->expr()->trim(
                'fixed_title',
                TrimMode::TRAILING,
                ' '
            ),
            'fixed_title',
        ),
    )
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'),
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
// and ensures the same content across all supported database systems.
Copied!
Querying space-padded values in PostgreSQL 
Retrieve with space-padded value for PostgreSQL does not retrieve the record
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// PostgreSQL specific query!

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1    '), // untrimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows === []
Copied!

Additional tools for consistent behaviour 

Additional ExpressionBuilder methods can be used to ensure consistent behaviour across all supported platforms:

  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::trim()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::rightPad()

Recommendation 

CHAR and BINARY fields can be used for storage or performance adjustments, but only when composed data and queries account for the differences between database systems.

Otherwise, the safe bet is to consistently use VARCHAR and VARBINARY column types.

Important: #105538 - Plugin subtypes removed: Changes to configurePlugin() and TCA handling 

See forge#105538

Description 

Due to the removal of the plugin content element "Plugin" (list) and the corresponding plugin subtype field list_type, the fifth parameter $pluginType of ExtensionUtility::configurePlugin() is now unused and can be omitted. It is only kept for backwards compatibility.

Be aware that passing any value other than CType will trigger an \InvalidArgumentException.

Please also note that due to the removal of the list_type field in tt_content, passing list_type as the second parameter $field to ExtensionManagementUtility::addTcaSelectItemGroup() will now, as for any other non-existent field, trigger a \RuntimeException.

Important: #106192 - Add 'center' and 'font' to YAML processing removeTags 

See forge#106192

Description 

The HTML tags <font> and <center> have been officially deprecated for some time: see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/font and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/center.

The default YAML processing configuration file EXT:rte_ckeditor/Configuration/RTE/Processing.yaml has been changed to remove these HTML tags <font> and <center> by default when saving RTE field contents to the database.

This new default is adjusted with the option processing.HTMLparser_db.removeTags, which now also lists these two tags.

A stored input like <p><font face="Arial">My text</font></p> will - when saved - be changed to <p>My text</p>.

Affected installations 

All installations having <font> and <center> stored in their database fields, and where no custom RTE YAML configuration is in place that allows these tags.

Please note that due to issue forge#104839, this removeTags option was never properly applied previously, so the chances are that an installation never had output for font and center properly working anyway.

Also, note that CKEditor by default uses <span style="..."> tags to apply font formatting when using the Full preset.

Thus, the real-life impact should be low, but for legacy installations, you may want to convert existing data to replace font/html tags with their appropriate modern counterparts.

Migration 

Either accept the removal of these tags and use specific HTML tags like <span> and <div> to apply formatting.

Or adapt the RTE processing via TypoScript/YAML configuration to not have center and font in the processing.HTMLparser_db.removeTags list.

If the tags center and font have been configured via the editor.conf.style.definitions YAML option (not set by default), CKEditor would allow the use of these tags, but they will now be removed both when saving or when being rendered in the frontend. So these style definitions should be removed and/or adapted to <span style="..."> configurations.

Important: #106532 - Changed database storage format for Scheduler Tasks 

See forge#106532

Description 

TYPO3's system extension scheduler has stored its tasks to be executed in a PHP-serialized format in the database since its inception.

This has led to many problems, for example, when changing a class property to be fully typed, when a class name changed to use PHP 5 namespaces, or when renaming a class or a class property.

This has now changed: the task object now stores the "tasktype" (typically the class name) and its options in a "parameters" JSON-encoded value, as well as the execution details (database field execution_details) in separate fields of the database table tx_scheduler_task. This way, the task object can be reconstituted through a properly defined API, avoiding issues in the future.

All existing tasks are compatible with the new internal format. An upgrade wizard ensures that the previously serialized objects are transferred into the new format. If this wizard does not disappear after being executed, it means there are tasks that failed to migrate and may need manual inspection or recreation. Inspect all tasks in tx_scheduler_task where the "tasktype" column is empty. The old serialized data format is somewhat human-readable (or can be inspected with PHP deserializers), so recreating a task with its former configuration options should be possible.

Please note that this upgrade step needs to be performed in the context of TYPO3 v14. Running the wizard in future TYPO3 versions may not succeed due to changes in the task objects.

Important: #106649 - Default language binding in layout module 

See forge#106649

Description 

The Content > Layout module now always uses default language binding ( mod.web_layout.defLangBinding) when displaying localized content in language comparison mode.

Default language binding makes editing translations easier: Editors can see what they are translating next to the default language. It also prevents confusion when localizations are incomplete. Editors can directly see which content is not yet translated. This improves the user experience in the backend for multilingual sites, which was also a result of recent "Jobs To Be Done" (JTBD) interviews.

Impact 

Editors will now always see the content elements next to each other within the Content > Layout module when in language comparison mode.

Migration 

Because default language binding is now always enabled, the previous Page TSconfig setting mod.web_layout.defLangBinding is not evaluated and can therefore be removed.

Important: #106656 - Allow DEFAULT NULL for varchar fields 

See forge#106656

Description 

In TCA, if an input field is configured to be nullable via 'nullable' => true, the database migration now respects this and creates or updates existing fields with DEFAULT NULL.

In Extbase, this may cause issues if properties and their accessor methods are not properly declared as nullable. Therefore, this change is introduced only in TYPO3 v14.

Example:

Example properly implementing a nullable property
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyExtbaseEntity extends AbstractEntity
{
    protected ?string $title;

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(?string $title): void
    {
        $this->title = $title;
    }
}
Copied!

As stated above, this automatic detection is not provided in TYPO3 versions earlier than 14.0. Using DEFAULT NULL can be enforced via an extension's ext_tables.sql instead:

CREATE TABLE tx_myextension_table (
    title varchar(255) DEFAULT NULL
);
Copied!

Important: #106947 - Allow extensions to ship upgrade wizards without requiring EXT:install 

See forge#106947

Description 

EXT:install is no longer installed per default in composer based minimal installations.

However, this advantage could often not be utilised sensibly, since extensions still needed to require EXT:install as a dependency in order to ship upgrade wizards, because the implemented interfaces needed to be available.

The basic required interfaces and the PHP attribute are now moved to the EXT:core namespace. The old counterparts are deprecated with Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core but are still provided by EXT:install for the time being.

The following changes are required (when requiring TYPO3 14.0+) to make EXT:install optional:

  • Let custom upgrade wizards implement \TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface .
  • Implement custom list-type to CType migrations extending \TYPO3\CMS\Core\Upgrades\AbstractListTypeToCTypeUpdate .
  • Use the attribute \TYPO3\CMS\Core\Attribute\UpgradeWizard to register custom upgrade wizards.

Extension authors supporting two major TYPO3 versions with one extension version can follow these strategies:

  • TYPO3 v13 and v14: Use deprecated EXT:install interfaces and PHP Attribute and require EXT:install as mandatory dependency, or add it as a suggestion to allow the decision to be made on project-level.
  • TYPO3 v14: Switch to EXT:core interfaces and the new PHP Attribute.

See Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core for details about moved interfaces, PHP attribute and a migration example.

Important: #107328 - $GLOBALS['TCA'] in base TCA files 

See forge#107328

Description 

The backward compatibility for using $GLOBALS['TCA'] in base TCA files has been removed. Base TCA files are the first TCA files loaded by the Core and do not have the fully loaded TCA available yet. Until now, this worked because the Core temporarily populated this global array during the loading process.

Note that the usage of $GLOBALS['TCA'] in base TCA files was never explicitly allowed or disallowed, only discouraged. It worked only due to internal system knowledge by the user, for example, knowing that the loading order of extensions affects when those files are loaded. The only place this array should be used is inside TCA/Overrides/*.php files. For all other cases, the TcaSchema should be preferred.

Migration 

In the uncommon case that you find usages of $GLOBALS['TCA'] in base TCA files, move that access to TCA/Overrides/*.php files instead.

Important: #107399 - Add more common file types to mediafile_ext 

See forge#107399

Description 

The default configuration for $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] has been extended with additional formats commonly uploaded to web-based CMS systems such as TYPO3:

  • 3gp
  • aac
  • aif
  • avif
  • heic
  • ico
  • m4a
  • m4v
  • mov
  • psd

This list is extended only for systems where the configuration option has not already been customized.

Adding these formats allows files with these extensions to be uploaded into the file list (and used in TCA fields with "common-media-types") when the security flag security.system.enforceAllowedFileExtensions is enabled.

This may be a relevant change if these file types are now used in records or content elements that do not expect such files for further operations (for example, in frontend display).

Integrators must ensure that all uploaded file types are handled appropriately (for example, embedding "mov" as a video, "ico" as an image, or "psd" as a download) or have a suitable fallback.

The <f:media> ViewHelper, for example, iterates over possible Renderer implementations that can handle several of these file types (and their MIME types) and attempts to render a file as an image as a fallback.

Important: #107536 - Install Tool now adapts to backend login routing 

See forge#107536

Description 

The Install Tool now integrates with TYPO3's backend routing system instead of using a separate typo3/install.php file. This modernization improves consistency while maintaining full backward compatibility.

If the TYPO3 installation is not working properly, the Install Tool can now be accessed via the ?__typo3_install parameter, ensuring administrators can rely on it for system maintenance and recovery.

Impact 

For Administrators:

All existing workflows continue to work without changes. However, the Install Tool is now accessible via:

  • The __typo3_install parameter (for example, https://example.com/?__typo3_install)
  • Backend routes such as /typo3/install and /typo3/install.php still work. If the $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] is set, the Install Tool adapts accordingly.

System Recovery:

When the TYPO3 installation is not set up or not working properly, the magic __typo3_install parameter still redirects to the installer or maintenance tool as before, ensuring administrators can always access system recovery tools.

Technical Benefits:

The Install Tool now uses the same routing infrastructure as the rest of the TYPO3 backend, creating a more unified and maintainable architecture while supporting the long-term goal of simplifying TYPO3's directory structure.

Migration 

No migration is required. All existing documentation, scripts, and workflows using the Install Tool continue to function without modification.

Important: #107629 - Reference Index check moved to Install Tool 

See forge#107629

Description 

The Check and Update Reference Index functionality has been moved from the System > DB Check module (EXT:lowlevel) to the Maintenance section of the Install Tool (EXT:install) in the top level module now called System.

This change makes this essential administrative tool more accessible and better organized alongside other common maintenance tasks such as database comparison, cache management, and folder structure checks.

Why this change? 

The Reference Index is a critical system component that tracks relationships between records in TYPO3. Checking and updating is a routine maintenance task that administrators perform regularly, similar to:

  • Analyzing database structure
  • Clearing caches
  • Checking folder permissions

Previously, this functionality was hidden in the DB Check module of EXT:lowlevel, which made it:

  • Hard to discover: Administrators had to know where to look
  • Inconsistent: Other maintenance tools were in the Install Tool
  • Less accessible: Required an additional system extension

Impact 

For Administrators:

The Reference Index check and update functionality is now directly available in the Install Tool under Maintenance > Check and Update Reference Index.

Key benefits:

  • Better visibility: Found alongside other maintenance tools
  • No extra dependencies: Works out-of-the-box without EXT:lowlevel
  • Consistent location: All system maintenance tasks in one place
  • Faster access: Direct access via the Install Tool
  • Same functionality: Check and update operations work exactly as before

CLI Access:

The command-line interface remains unchanged and continues to work as before:

# Check reference index
vendor/bin/typo3 referenceindex:update --check

# Update reference index
vendor/bin/typo3 referenceindex:update
Copied!

Migration 

No migration is required. System Maintainers should use the new location in the System > Maintenance section instead of the System > DB Check module.

The functionality works identically to the previous implementation.

Important: #107735 - Internal methods removed from ResourceFactory 

See forge#107735

Description 

The following internal methods have been removed from \TYPO3\CMS\Core\Resource\ResourceFactory :

  • getDefaultStorage()
  • getStorageObject()
  • createFolderObject()
  • getFileObjectByStorageAndIdentifier()

These methods were marked as @internal and are replaced by using \TYPO3\CMS\Core\Resource\StorageRepository directly for better separation of concerns. However, some of these methods might have been used in custom extensions despite being marked as internal.

Migration 

Instead of using the removed methods from ResourceFactory, use the appropriate methods from StorageRepository or direct access to ResourceStorage:

// Before
$defaultStorage = $resourceFactory->getDefaultStorage();
$storage = $resourceFactory->getStorageObject($uid);
$folder = $resourceFactory->createFolderObject($storage, $identifier, $name);
$file = $resourceFactory->getFileObjectByStorageAndIdentifier($storage, $fileIdentifier);

// After
$defaultStorage = $storageRepository->getDefaultStorage();
$storage = $storageRepository->getStorageObject($uid);
$folder = $storage->getFolder($identifier);
$file = $storage->getFileByIdentifier($fileIdentifier);
Copied!

Important: #107789 - TCA tab labels consolidated into core.form.tabs 

See forge#107789

Description 

To improve consistency and maintainability of TCA tab labels across TYPO3 Core, commonly used tab labels from various extensions have been consolidated into the central EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf file.

This consolidation allows for better reusability and ensures consistent translation of common tab labels across all core extensions. It also makes it easier for extension developers to use standardized tab names.

New labels available in locallang_tabs.xlf 

The following new tab labels are now available and should be used via the core.form.tabs: prefix:

  • core.form.tabs:audio - "Audio"
  • core.form.tabs:video - "Video"
  • core.form.tabs:camera - "Camera"
  • core.form.tabs:permissions - "Permissions"
  • core.form.tabs:mounts - "Mounts"
  • core.form.tabs:personaldata - "Personal Data"

Previously existing labels (already migrated in core):

  • core.form.tabs:general - "General"
  • core.form.tabs:access - "Access"
  • core.form.tabs:categories - "Categories"
  • core.form.tabs:notes - "Notes"
  • core.form.tabs:language - "Language"
  • core.form.tabs:extended - "Extended"
  • core.form.tabs:appearance - "Appearance"
  • core.form.tabs:behaviour - "Behavior"
  • core.form.tabs:metadata - "Metadata"
  • core.form.tabs:resources - "Resources"
  • core.form.tabs:seo - "SEO"
  • core.form.tabs:socialmedia - "Social Media"
  • core.form.tabs:options - "Options"

Migrated extension-specific labels 

The following extension-specific tab labels have been migrated to the consolidated labels file and are marked as unused since TYPO3 v14.0 with the attribute x-unused-since="14.0" in the corresponding XLF files.

EXT:filemetadata

  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata core.form.tabs:metadata
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.camera core.form.tabs:camera
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.audio core.form.tabs:audio
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.video core.form.tabs:video

EXT:seo

  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.seo core.form.tabs:seo
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.socialmedia core.form.tabs:socialmedia

EXT:core - Backend Users

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data core.form.tabs:personaldata
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.options core.form.tabs:options

EXT:core - Backend User Groups

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.options core.form.tabs:options

EXT:frontend - Frontend Users

  • LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData core.form.tabs:personaldata

Affected installations 

Custom extensions using TCA configurations may benefit from using the new consolidated tab labels instead of creating their own labels for common tab names.

Extensions that were using any of the migrated extension-specific labels listed above will continue to work in TYPO3 v14.0, but should migrate to the consolidated labels. The old labels will be removed in TYPO3 v15.0.

Migration 

For custom extensions, consider using the consolidated core.form.tabs: labels instead of creating custom labels for common tab names.

Example migration for extensions using old labels:

File metadata tabs

// Before
'--div--;LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata'

// After
'--div--;core.form.tabs:metadata'
Copied!

User and group tabs

// Before
'--div--;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data'
'--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData'

// After
'--div--;core.form.tabs:personaldata'
Copied!

Using consolidated labels in custom extensions

// Example: Custom TCA using consolidated labels
'types' => [
    '1' => [
        'showitem' => '
            --div--;core.form.tabs:general,
                title, description,
            --div--;core.form.tabs:metadata,
                author, keywords,
            --div--;core.form.tabs:access,
                hidden, starttime, endtime,
            --div--;core.form.tabs:categories,
                categories,
            --div--;core.form.tabs:extended,
        ',
    ],
],
Copied!

New labels available in palettes.xlf 

In addition to tab labels, commonly used palette labels have also been consolidated into the central EXT:core/Resources/Private/Language/Form/palettes.xlf file.

The following palette labels are now available via the core.form.palettes: prefix:

  • core.form.palettes:general - "General"
  • core.form.palettes:account - "Account"
  • core.form.palettes:authentication - "Authentication"
  • core.form.palettes:permission_languages - "Language permissions"
  • core.form.palettes:permission_general - "General permissions"
  • core.form.palettes:permission_specific - "Specific permissions"
  • core.form.palettes:standard - "Page"
  • core.form.palettes:title - "Title"
  • core.form.palettes:visibility - "Visibility"
  • core.form.palettes:access - "Publish Dates and Access Rights"
  • core.form.palettes:abstract - "Abstract"
  • core.form.palettes:metatags - "Meta Tags"
  • core.form.palettes:editorial - "Editorial"
  • core.form.palettes:page_layout - "Page Layout"
  • core.form.palettes:use_as_container - "Use as Container"
  • core.form.palettes:replace - "Replace Content"
  • core.form.palettes:links - "Links to this Page"
  • core.form.palettes:caching - "Caching"
  • core.form.palettes:language - "Language"
  • core.form.palettes:miscellaneous - "Miscellaneous"
  • core.form.palettes:media - "Files"
  • core.form.palettes:storage - "Storage Page"
  • core.form.palettes:config - "Configuration"
  • core.form.palettes:headers - "Headlines"
  • core.form.palettes:header - "Header"
  • core.form.palettes:content_layout - "Content Element Layout"
  • core.form.palettes:media_behaviour - "Media Behaviour"
  • core.form.palettes:accessibility - "Accessibility"
  • core.form.palettes:downloads_layout - "Downloads Layout"
  • core.form.palettes:table_layout - "Table Layout"
  • core.form.palettes:links_appearance - "Links"
  • core.form.palettes:settings_gallery - "Gallery Settings"
  • core.form.palettes:media_adjustments - "Media Adjustments"
  • core.form.palettes:metrics - "Metrics"
  • core.form.palettes:geolocation - "Geo Location"
  • core.form.palettes:contentdate - "Content Date"
  • core.form.palettes:gps - "GPS"
  • core.form.palettes:seo - "General SEO settings"
  • core.form.palettes:robots - "Robot instructions"
  • core.form.palettes:opengraph - "Open Graph (Facebook)"
  • core.form.palettes:twittercards - "X / Twitter Cards"
  • core.form.palettes:canonical - "Canonical"
  • core.form.palettes:sitemap - "Sitemap"
  • core.form.palettes:additional - "Additional configuration"

Migrated palette labels 

The following palette labels have been migrated to use core.form.palettes:* and are marked as unused since TYPO3 v14.0 (attribute x-unused-since="14.0" in XLF files):

EXT:core - Backend Users & Groups:

  • be_users.palettes.account, be_users.palettes.authentication, be_users.palettes.permissionLanguages
  • be_groups.palettes.authentication, be_groups.palettes.permissionGeneral, be_groups.palettes.permissionLanguages, be_groups.palettes.permissionSpecific

EXT:frontend - Pages & Content Elements:

  • pages.palettes.* (17 labels: standard, title, visibility, access, abstract, metatags, editorial, layout, module, replace, links, caching, language, miscellaneous, media, storage, config)
  • palette.* in tt_content (13 labels: general, headers, header, visibility, access, frames, imagelinks, image_accessibility, uploads_layout, table_layout, appearanceLinks, gallerySettings, mediaAdjustments)

EXT:filemetadata:

  • palette.* (6 labels: metrics, geo_location, visibility, content_date, accessibility, gps)

EXT:seo:

  • pages.palettes.* (6 labels: seo, robots, opengraph, twittercards, canonical, sitemap)

Important: #107848 - DataHandler properties userid and admin removed 

See forge#107848

Description 

The internal properties \TYPO3\CMS\Core\DataHandling\DataHandler::$userid and \TYPO3\CMS\Core\DataHandling\DataHandler::$admin have been removed.

These properties contained information that is already available through the BE_USER property and were therefore redundant.

Impact 

Accessing these properties directly will result in a fatal PHP error.

Affected installations 

All installations with extensions that access the following properties:

  • \TYPO3\CMS\Core\DataHandling\DataHandler::$userid
  • \TYPO3\CMS\Core\DataHandling\DataHandler::$admin

While these properties were marked as @internal, they have been commonly used by extensions, especially the $admin property.

Migration 

Replace any usage of these properties with the appropriate methods from the BE_USER property.

For $userid 

// Before:
$userId = $dataHandler->userid;

// After:
$userId = $dataHandler->BE_USER->getUserId();
Copied!

For $admin 

// Before:
if ($dataHandler->admin) {
    // do something
}

// After:
if ($dataHandler->BE_USER->isAdmin()) {
    // do something
}
Copied!

14.x Changes by type 

This lists all changes to the TYPO3 Core of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

ChangeLog v13 

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

Also available 

13.4.x Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.4.0 LTS release.

Features 

Deprecation 

Important 

Feature: #105638 - Modify fetched page content 

See forge#105638

Description 

With forge#103894 the new data processor PageContentFetchingProcessor has been introduced, to allow fetching page content based on the current page layout, taking the configured SlideMode into account.

Fetching content has previously mostly been done via the Content content object. A common example looked like this:

page.20 = CONTENT
page.20 {
    table = tt_content
    select {
        orderBy = sorting
        where = colPos=0
    }
}
Copied!

As mentioned in the linked changelog, using the page-content data processor, this can be simplified to:

page.20 = page-content
Copied!

This however reduces the possibility to modify the select configuration (SQL statement), used to define which content should be fetched, as this is automatically handled by the data processor. However, there might be some use cases in which the result needs to be adjusted, e.g. to hide specific page content, like it's done by EXT:content_blocks for child elements. For such use cases, the new PSR-14 AfterContentHasBeenFetchedEvent has been introduced, which allows to manipulate the list of fetched page content.

The following member properties of the event object are provided:

  • $groupedContent: The fetched page content, grouped by their column - as defined in the page layout
  • $request: The current request, which can be used to e.g. access the page layout in question

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, removes some of the fetched page content elements based on specific field values.

my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(AfterContentHasBeenFetchedEvent $event): void
    {
        foreach ($event->groupedContent as $columnIdentifier => $column) {
            foreach ($column['records'] ?? [] as $key => $record) {
                if ($record->has('parent_field_name') && (int)($record->get('parent_field_name') ?? 0) > 0) {
                    unset($event->groupedContent[$columnIdentifier]['records'][$key]);
                }
            }
        }
    }
}
Copied!

Impact 

Using the new PSR-14 AfterContentHasBeenFetchedEvent, it's possible to manipulate the page content, which has been fetched by the PageContentFetchingProcessor, based on the page layout and corresponding columns configuration.