TYPO3 Explained 

API A-Z 

This chapter describes the core functionality of TYPO3 including API and framework configuration.

Coding guidelines 

This chapter contains a description of the formal requirements or standards regarding coding that you should adhere to when you develop TYPO3 extensions or Core parts.

Extension development 

Learn how to write site packages and other custom extension with or without Extbase.

PHP architecture 

General PHP architectural strategies in TYPO3 core development and best practices for extension developers.

Security guidelines 

This chapter describes some typical risks and advises how to protect a TYPO3 site in order to ensure it is and stays secure and stable.

Automated testing 

This chapter goes into details about automatic testing: Writing, maintaining and running them in various scopes.

Introduction 

TYPO3 is a content management system based on PHP. The TYPO3 - Getting Started Tutorial gives you an introduction to the core concepts of TYPO3 and will help you to kickstart your first project.

A basic TYPO3 installation 

Installation with Composer is covered in the Getting Started Guide (see Installation chapter).

You can also download TYPO3 from our official page

If you are starting out we would suggest using the latest TYPO3 version with long term support.

A basic site package 

To get started you need a basic site package.

You can use the official Site Package Builder to generate one for you or follow the site package tutorial.

Getting help with TYPO3 

Meet the TYPO3 Community. You can chat with us on Slack, go to meetings in your local user group or meet us at events.

You can discuss TYPO3 related questions in the https://talk.typo3.org/ forum.

If you think you have found a bug in the TYPO3 Core code, have a look at our issue tracker on forge where you can check if the bug has already been reported or not.

API A-Z 

The TYPO3 Core code serves a dual purpose: It functions both as an out-of-the-box application and as a library providing APIs for extensions that enhance projects with additional functionality.

The TYPO3 Core itself organizes its code into extensions as well, with the "core" extension offering the majority of API classes. These classes are utilized by other key extensions such as "frontend" and "backend". These three extensions are mandatory for any TYPO3-based project, while others, like "scheduler," are optional.

This chapter focuses on the APIs primarily provided by these three essential extensions.

TYPO3 APIs are primarily documented within the source code itself. Maintaining documentation in multiple locations is impractical due to the frequent changes in the codebase. This chapter highlights the most critical elements of the API.

Contents:

Assets (CSS, JavaScript, Media) 

Introduction 

The TYPO3 component responsible for rendering the HTML and adding assets to a TYPO3 frontend or backend page is called \TYPO3\CMS\Core\Page\PageRenderer .

The PageRenderer collects all assets to be rendered, takes care of options such as concatenation or compression and finally generates the necessary tags.

There are multiple ways to add assets to the PageRenderer in TYPO3. For configuration options via TypoScript (usually used for the main theme files), see the TypoScript Reference. In extensions, both directly using the PageRenderer as well as using the more convenient AssetCollector is possible.

Asset collector 

With the \TYPO3\CMS\Core\Page\AssetCollector class, CSS and JavaScript code (inline or external) can be added multiple times, but rendered only once in the output. The class may be used directly in PHP code or the assets can be added via the <f:asset.css> and <f:asset.script> ViewHelpers.

The priority flag (default: false) controls where the asset is inserted:

  • JavaScript will be output inside <head> if $priority == true, or at the bottom of the <body> tag if $priority == false.
  • CSS will always be output inside <head>, yet grouped by $priority.

The asset collector helps to work with content elements as components, effectively reducing the CSS to be loaded. It takes advantage of HTTP/2, which removes the necessity to concatenate all files in one file.

The asset collector class is implemented as a singleton ( \TYPO3\CMS\Core\SingletonInterface ). It replaces various other existing options in TypoScript and methods in PHP for inserting JavaScript and CSS code.

The asset collector also collects information about images on a page, which can be used in cached and non-cached components.

New in version 13.3

Option external to skip URL processing in AssetRenderer has been added.

The AssetCollector option external can be used for asset files using $assetCollector->addStyleSheet() or $assetCollector->addJavaScript(). If set all processing of the asset URI (like the addition of the cache busting parameter) is skipped and the input path will be used as-is in the resulting HTML tag.

The API 

class AssetCollector
Fully qualified name
\TYPO3\CMS\Core\Page\AssetCollector

The Asset Collector is responsible for keeping track of - everything within <script> tags: javascript files and inline javascript code - inline CSS and CSS files

The goal of the asset collector is to: - utilize a single "runtime-based" store for adding assets of certain kinds that are added to the output - allow to deal with assets from non-cacheable plugins and cacheable content in the Frontend - reduce the "power" and flexibility (I'd say it's a burden) of the "god class" PageRenderer. - reduce the burden of storing everything in PageRenderer

As a side effect this allows to: - Add a single CSS snippet or CSS file per content block, but assure that the CSS is only added once to the output.

Note on the implementation: - We use a Singleton to make use of the AssetCollector throughout Frontend process (similar to PageRenderer). - Although this is not optimal, I don't see any other way to do so in the current code.

addJavaScript ( string $identifier, string $source, array $attributes = [], array $options = [])
param $identifier

the identifier

param $source

URI to JavaScript file (allows EXT: syntax)

param $attributes

additional HTML <script> tag attributes, default: []

param $options

['priority' => true] means rendering before other tags, default: []

Returns
self
addJavaScriptModule ( string $identifier)
param $identifier

Bare module identifier like @my/package/Filename.js

Returns
self
addInlineJavaScript ( string $identifier, string $source, array $attributes = [], array $options = [])
param $identifier

the identifier

param $source

JavaScript code

param $attributes

additional HTML <script> tag attributes, default: []

param $options

['priority' => true] means rendering before other tags, default: []

Returns
self
addStyleSheet ( string $identifier, string $source, array $attributes = [], array $options = [])
param $identifier

the identifier

param $source

URI to stylesheet file (allows EXT: syntax)

param $attributes

additional HTML <link> tag attributes, default: []

param $options

['priority' => true] means rendering before other tags, default: []

Returns
self
addInlineStyleSheet ( string $identifier, string $source, array $attributes = [], array $options = [])
param $identifier

the identifier

param $source

stylesheet code

param $attributes

additional HTML <link> tag attributes, default: []

param $options

['priority' => true] means rendering before other tags, default: []

Returns
self
addMedia ( string $fileName, array $additionalInformation)
param $fileName

the fileName

param $additionalInformation

One dimensional hash map (array with non-numerical keys) with scalar values

Returns
self
removeJavaScript ( string $identifier)
param $identifier

the identifier

Returns
self
removeInlineJavaScript ( string $identifier)
param $identifier

the identifier

Returns
self
removeStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
self
removeInlineStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
self
removeMedia ( string $identifier)
param $identifier

the identifier

Returns
self
getMedia ( )
Returns
array
getJavaScripts ( ?bool $priority = NULL)
param $priority

the priority, default: NULL

Returns
array
getInlineJavaScripts ( ?bool $priority = NULL)
param $priority

the priority, default: NULL

Returns
array
getJavaScriptModules ( )
Returns
array
getStyleSheets ( ?bool $priority = NULL)
param $priority

the priority, default: NULL

Returns
array
getInlineStyleSheets ( ?bool $priority = NULL)
param $priority

the priority, default: NULL

Returns
array
hasJavaScript ( string $identifier)
param $identifier

the identifier

Returns
bool
hasInlineJavaScript ( string $identifier)
param $identifier

the identifier

Returns
bool
hasStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
bool
hasInlineStyleSheet ( string $identifier)
param $identifier

the identifier

Returns
bool
hasMedia ( string $fileName)
param $fileName

the fileName

Returns
bool

ViewHelper 

There are also two ViewHelpers, the f:asset.css and the f:asset.script ViewHelper which use the AssetCollector API.

Rendering order 

Currently, CSS and JavaScript registered with the asset collector will be rendered after their page renderer counterparts. The order is:

  • <head>
  • page.includeJSLibs.forceOnTop
  • page.includeJSLibs
  • page.includeJS.forceOnTop
  • page.includeJS
  • AssetCollector::addJavaScript() with 'priority'
  • page.jsInline
  • AssetCollector::addInlineJavaScript() with 'priority'
  • </head>
  • page.includeJSFooterlibs.forceOnTop
  • page.includeJSFooterlibs
  • page.includeJSFooter.forceOnTop
  • page.includeJSFooter
  • AssetCollector::addJavaScript()
  • page.jsFooterInline
  • AssetCollector::addInlineJavaScript()

Examples 

The AssetCollector can be injected in the constructor of a class via dependency injection and then used in methods:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Page\AssetCollector;

final class MyClass
{
    public function __construct(
        private readonly AssetCollector $assetCollector,
    ) {}

    public function doSomething()
    {
        // $this->assetCollector can now be used
        // see examples below
    }
}
Copied!

Add a JavaScript file to the collector with script attribute data-foo="bar":

EXT:my_extension/Classes/MyClass.php
$this->assetCollector->addJavaScript(
    'my_ext_foo',
    'EXT:my_extension/Resources/Public/JavaScript/foo.js',
    ['data-foo' => 'bar']
);
Copied!

Add a JavaScript file to the collector with script attribute data-foo="bar" and a priority which means rendering before other script tags:

EXT:my_extension/Classes/MyClass.php
$this->assetCollector->addJavaScript(
    'my_ext_foo',
    'EXT:my_extension/Resources/Public/JavaScript/foo.js',
    ['data-foo' => 'bar'],
    ['priority' => true]
);
Copied!

Add a JavaScript file to the collector with type="module" (by default, no type= is output for JavaScript):

EXT:my_extension/Classes/MyClass.php
$this->assetCollector->addJavaScript(
    'my_ext_foo',
    'EXT:my_extension/Resources/Public/JavaScript/foo.js',
    ['type' => 'module']
);
Copied!

Check if a JavaScript file with the given identifier exists:

EXT:my_extension/Classes/MyClass.php
if ($this->assetCollector->hasJavaScript($identifier)) {
    // result: true - JavaScript with identifier $identifier exists
} else {
    // result: false - JavaScript with identifier $identifier does not exist
}
Copied!

The following code skips the cache busting parameter ?1726090820 for the supplied CSS file:

EXT:my_extension/Classes/MyClass.php
$assetCollector->addStyleSheet(
    'myCssFile',
    PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:my_extension/Resources/Public/MyFile.css')),
    [],
    ['external' => true]
);
Copied!

Resulting in the following HTML output:

<link rel="stylesheet" href="/_assets/<hash>/myFile.css" />
Copied!

Events 

There are two events available that allow additional adjusting of assets:

Former methods to add assets 

Using the page renderer 

An instance of the PageRenderer class can be injected into the class via dependency injection:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\MyClass;

use TYPO3\CMS\Core\Page\PageRenderer;

final class MyClass
{
    public function __construct(
        private readonly PageRenderer $pageRenderer,
    ) {}

    public function doSomething()
    {
        // $this->pageRenderer can now be used
        // see examples below
    }
}
Copied!

The following methods can then be used:

  • $this->pageRenderer->addHeaderData($javaScriptCode)
  • $this->pageRenderer->addCssFile($file)
  • $this->pageRenderer->addCssInlineBlock($name, $cssCode)
  • $this->pageRenderer->addCssLibrary($file)
  • $this->pageRenderer->addJsFile($file)
  • $this->pageRenderer->addJsFooterFile($file)
  • $this->pageRenderer->addJsFooterLibrary($name, $file)
  • $this->pageRenderer->addJsFooterInlineCode($name, $javaScriptCode)
  • $this->pageRenderer->addJsInlineCode($name, $javaScriptCode)
  • $this->pageRenderer->addJsLibrary($name, $file)

Using the TypoScriptFrontendController 

Changed in version 13.0

The property additionalHeaderData has been marked as internal and should not be used. Use AssetCollector->addJavaScript() instead (like described in the examples above).

Deprecated since version 13.4

The class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

$GLOBALS['TSFE']->additionalHeaderData[$name] = $javaScriptCode;
Copied!

Authentication: Backend and frontend users 

In TYPO3, there are two distinct user types:

Backend user 

These users can log into the TYPO3 backend to manage content and configure settings based on their assigned permissions.

Frontend user 

These users can log into the TYPO3 frontend to access restricted content that is not publicly available.

The following topics are of interest when you are working with backend or frontend users in TYPO3:

Authentication services 

Authentication services in TYPO3 manage user verification, allowing flexible and customizable login methods for secure access control.

Password policies 

The password policy validator is used to validate passwords against configurable password policies.

Sessions 

The user session contains all information for logged in backend and frontend users, as well as anonymous visitors without login. It can be used to store session data like a shopping basket.

Multi-factor authentication 

Enhance your TYPO3 security with Multi-Factor Authentication (MFA), adding an extra layer of protection beyond passwords for safer logins.

Backend user API 

In TYPO3, backend users (BE users) are responsible for managing content, settings, and administration tasks within the backend. They are stored in the be_users database table and authenticated via the Backend user object stored in the global variable $GLOBALS['BE_USER'] (class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication ).

Sudo mode (step-up authentication) for password changes 

New in version 12.4.32 / 13.4.13

This functionality was introduced in response to security advisory TYPO3-CORE-SA-2025-013 to mitigate password-change risks.

This mechanism prevents unauthorized password changes if an administrator session is hijacked or left unattended.

When an administrator edits their own user account or changes the password of another user via the admin interface, password confirmation (step-up authentication) is required.

Dialog "Verify with user password" with password prompt shown on attempting to change a password.

Step-up authentication requires the administrator to re-enter their password

Frontend user API 

In TYPO3, frontend users (FE users) are responsible for accessing restricted content and personalized areas of a TYPO3 website. They are stored in the fe_users database table and authenticated via the frontend.user request attribute (class \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication ).

The frontend user id and groups are available from the User aspect (Context API).

The system extension typo3/cms-felogin offers a plugin that can included in the page so that frontend users can login and logout. It also features a password-forgotten workflow.

The TYPO3 Core does not provide a plugin to register frontend users. There are multiple third-party extensions available in the TER, for example evoweb/sf-register .

Session data 

In the TYPO3 frontend, data can be stored in the current user session.

The actual location where the data will be stored is determined by the Session storage configuration.

You can use the following methods in FrontendUserAuthentication :

FrontendUserAuthentication::getKey($type, $key)
Loads data from the session.
FrontendUserAuthentication::setKey($type, $key, $data)
Saves data to the session as a string.
FrontendUserAuthentication::storeSessionData()
Writes session data so it is available in the next request.

Example: Save shopping basket into user session 

Let us assume we have an Extbase Controller for a shopping basket. We want to preserve the data the user enters before closing the browser window. This should also work for non logged-in users.

We can use $this->request->getAttribute('frontend.user') to create a frontend user on the fly if none exists.

If no Frontend user is currently logged in, an anonymous frontend user will be created on the fly.

In the browser of the current user a session cookie will be set linking them to the anonymous frontend user.

packages/my_extension/Classes/Controller/ShoppingCartController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;

class ShoppingCartController extends ActionController
{
    public const FORM_SESSION = 'myextension_cart';

    public function putItemAction(Item $item): ResponseInterface
    {
        // Fetch cart from session or create a new one
        $cart = $this->getCartFromSession() ?? new Cart();
        $cart->add($item);
        $this->storeCartInSession($cart);
        return $this->redirect('list');
    }

    public function list(): ResponseInterface
    {
        $this->view->assign('cart', $this->getCartFromSession());
        return $this->htmlResponse();
    }

    private function getFrontendUser(): FrontendUserAuthentication
    {
        // This will create an anonymous frontend user if none is logged in
        return $this->request->getAttribute('frontend.user');
    }

    private function storeCartInSession(Cart $cart): void
    {
        // We use type ses to store the data in the session
        $this->getFrontendUser()->setKey('ses', self::FORM_SESSION, serialize($cart));
        // Important: store session data! Or it is not available in the next request!
        $this->getFrontendUser()->storeSessionData();
    }

    private function getCartFromSession(): ?Cart
    {
        $data = $this->getFrontendUser()->getKey('ses', self::FORM_SESSION);
        if (is_string($data)) {
            $cart = unserialize($data);
            if ($cart instanceof Cart) {
                return $cart;
            }
        }
        return null;
    }
}
Copied!

User session management 

User sessions in TYPO3 are represented as UserSession objects. The TYPO3 authentication service chain creates or updates user sessions when authenticating users.

The UserSession object contains all information regarding a user's session, for website visitors with session data (e.g. basket for anonymous / not-logged-in users), for frontend users as well as authenticated backend users. These are for example, the session id, the session data, if a session was updated, if the session is anonymous, or if it is marked as permanent and so on.

The UserSession object can be used to change and retrieve information in an object-oriented way.

For creating UserSession objects the UserSessionManager must be used since this manager acts as the main factory for user sessions and therefore handles all necessary tasks like fetching, evaluating and persisting them. It effectively encapsulates all calls to the SessionManager which is used for the session backend.

Public API of UserSessionManager 

The UserSessionManager can be retrieved using its static factory method create():

EXT:some_extension/Classes/Controller/SomeController.php
use TYPO3\CMS\Core\Session\UserSessionManager;

$loginType = 'BE'; // or 'FE' for frontend
$userSessionManager = UserSessionManager::create($loginType);
Copied!

You can then use the UserSessionManager to work with user sessions. A couple of public methods are available:

class UserSessionManager
Fully qualified name
\TYPO3\CMS\Core\Session\UserSessionManager

The purpose of the UserSessionManager is to create new user session objects (acting as a factory), depending on the need / request, and to fetch sessions from the session backend, effectively encapsulating all calls to the SessionManager.

The UserSessionManager can be retrieved using its static factory method create():

use TYPO3\CMS\Core\Session\UserSessionManager;

$loginType = 'BE'; // or 'FE' for frontend
$userSessionManager = UserSessionManager::create($loginType);
Copied!
createFromRequestOrAnonymous ( \Psr\Http\Message\ServerRequestInterface $request, string $cookieName)

Creates and returns a session from the given request. If the given $cookieName can not be obtained from the request an anonymous session will be returned.

param $request

the request

param $cookieName

Name of the cookie that might contain the session

Return description

An existing session if one is stored in the cookie, an anonymous session otherwise

Returns
\UserSession
createAnonymousSession ( )

Creates and returns an anonymous session object (which is not persisted)

Returns
\TYPO3\CMS\Core\Session\UserSession
hasExpired ( \TYPO3\CMS\Core\Session\UserSession $session)

Checks whether a session has expired. This is also the case if sessionLifetime is 0

param $session

the session

Returns
bool
willExpire ( \TYPO3\CMS\Core\Session\UserSession $session, int $gracePeriod)

Checks whether a given user session will expire within the given grace period

param $session

the session

param $gracePeriod

in seconds

Returns
bool
fixateAnonymousSession ( \TYPO3\CMS\Core\Session\UserSession $session, bool $isPermanent = false)

Persists an anonymous session without a user logged-in, in order to store session data between requests

param $session

The user session to fixate

param $isPermanent

If true, the session will get the ses_permanent flag, default: false

Return description

A new session object with an updated ses_tstamp (allowing to keep the session alive)

Returns
\UserSession
elevateToFixatedUserSession ( \TYPO3\CMS\Core\Session\UserSession $session, int $userId, bool $isPermanent = false)

Removes existing entries, creates and returns a new user session object.

See regenerateSession() below.

param $session

The user session to recreate

param $userId

The user id the session belongs to

param $isPermanent

If true, the session will get the ses_permanent flag, default: false

Return description

The newly created user session object

Returns
\UserSession
regenerateSession ( string $sessionId, array $existingSessionRecord = [], bool $anonymous = false)

Regenerates the given session. This method should be used whenever a user proceeds to a higher authorization level, for example when an anonymous session is now authenticated.

param $sessionId

The session id

param $existingSessionRecord

If given, this session record will be used instead of fetching again, default: []

param $anonymous

If true session will be regenerated as anonymous session, default: false

Returns
\TYPO3\CMS\Core\Session\UserSession
updateSessionTimestamp ( \TYPO3\CMS\Core\Session\UserSession $session)

Updates the session timestamp for the given user session if the session is marked as "needs update" (which means the current timestamp is greater than "last updated + a specified grace-time").

param $session

the session

Return description

A modified user session with a last updated value if needed

Returns
\UserSession
isSessionPersisted ( \TYPO3\CMS\Core\Session\UserSession $session)

Checks whether a given session is already persisted

param $session

the session

Returns
bool
removeSession ( \TYPO3\CMS\Core\Session\UserSession $session)

Removes a given session from the session backend

param $session

the session

updateSession ( \TYPO3\CMS\Core\Session\UserSession $session)

Updates the session data + timestamp in the session backend

param $session

the session

Returns
\TYPO3\CMS\Core\Session\UserSession
collectGarbage ( int $garbageCollectionProbability = 1)

Calls the session backends collectGarbage() method

param $garbageCollectionProbability

the garbageCollectionProbability, default: 1

create ( string $loginType, ?int $sessionLifetime = NULL, ?\TYPO3\CMS\Core\Session\SessionManager $sessionManager = NULL, ?\TYPO3\CMS\Core\Authentication\IpLocker $ipLocker = NULL)

Creates a UserSessionManager instance for the given login type. Has several optional arguments used for testing purposes to inject dummy objects if needed.

Ideally, this factory encapsulates all TYPO3_CONF_VARS options, so the actual object does not need to consider any global state.

param $loginType

the loginType

param $sessionLifetime

the sessionLifetime, default: NULL

param $sessionManager

the sessionManager, default: NULL

param $ipLocker

the ipLocker, default: NULL

Returns
static
setLogger ( \Psr\Log\LoggerInterface $logger)

Sets a logger.

param $logger

the logger

Public API of UserSession 

The session object created or retrieved by the UserSessionManager provides the following API methods:

class UserSession
Fully qualified name
\TYPO3\CMS\Core\Session\UserSession

Represents all information about a user's session.

A user session can be bound to a frontend / backend user, or an anonymous session based on session data stored in the session backend.

If a session is anonymous, it can be fixated by storing the session in the backend, but only if there is data in the session.

if a session is user-bound, it is automatically fixated.

The $isNew flag is meant to show that this user session object was not fetched from the session backend, but initialized in the first place by the current request.

The $data argument stores arbitrary data valid for the user's session.

A permanent session is not issued by a session-based cookie but a time-based cookie. The session might be persisted in the user's browser.

getIdentifier ( )
Return description

The session ID. This is the ses_id respectively the AbstractUserAuthentication->id

Returns
string
getUserId ( )
Return description

The user ID the session belongs to. Can also return 0 or NULL Which indicates an anonymous session. This is the ses_userid.

Returns
?int
getLastUpdated ( )
Return description

The timestamp of the last session data update. This is the ses_tstamp.

Returns
int
set ( string $key, ?mixed $value)

Sets or updates session data value for a given $key. It is also internally used if calling AbstractUserAuthentication->setSessionData()

param $key

The key whose value should be updated

param $value

The value or NULL to unset the key

hasData ( )

Checks whether the session has data assigned

Returns
bool
get ( string $key)

Returns the session data for the given $key or NULL if the key does not exist. It is internally used if calling AbstractUserAuthentication->getSessionData()

param $key

the key

getData ( )
Return description

The whole data array.

Returns
array
overrideData ( array $data)

Overrides the whole data array. Can also be used to unset the array.

This also sets the $wasUpdated pointer to true

param $data

the data

dataWasUpdated ( )

Checks whether the session data has been updated

Returns
bool
isAnonymous ( )

Checks if the user session is an anonymous one. This means, the session does not belong to a logged-in user

Returns
bool
getIpLock ( )
Return description

The ipLock state of the session

Returns
string
isNew ( )

Checks whether the session is marked as new

Returns
bool
isPermanent ( )

Checks whether the session was marked as permanent

Returns
bool
needsUpdate ( )

Checks whether the session has to be updated

Returns
bool
getJwt ( ?\TYPO3\CMS\Core\Http\CookieScope $scope = NULL)

Gets session ID wrapped in JWT to be used for emitting a new cookie.

Cookie: <JWT(HS256, [identifier => <session-id>], <signature(encryption-key, cookie-domain)>)>

param $scope

the scope, default: NULL

Return description

The session ID wrapped in JWT to be used for emitting a new cookie

Returns
string
createFromRecord ( string $id, array $record, bool $markAsNew = false)

Creates a new user session based on the provided session record

param $id

the session identifier

param $record

the record

param $markAsNew

the markAsNew, default: false

Returns
self
createNonFixated ( string $identifier)

Creates a non fixated user session. This means the session does not belong to a logged-in user

param $identifier

the identifier

Returns
self
resolveIdentifierFromJwt ( string $cookieValue, \TYPO3\CMS\Core\Http\CookieScope $scope)

Verifies and resolves the session ID from a submitted cookie value: Cookie: <JWT(HS256, [identifier => <session-id>], <signature(encryption-key, cookie-domain)>)>

param $cookieValue

submitted cookie value

param $scope

the scope

Return description

Session ID, null in case verification failed

Returns
non-empty-string|null

Session storage framework 

TYPO3 comes with the option to choose between different storages for both frontend and backend user sessions (called session backends).

The Core ships two session backends by default:

  • Database storage
  • Redis storage

By default user sessions are stored in the database using the database storage backend.

Database storage backend 

The database storage backend only requires two configuration options: The table name (table option) and whether anonymous sessions (has_anonymous option) may be stored.

The default configuration used for sessions by the Core is:

'SYS' => [
    'session' => [
        'BE' => [
            'backend' => \TYPO3\CMS\Core\Session\Backend\DatabaseSessionBackend::class,
            'options' => [
                'table' => 'be_sessions'
            ]
        ],
        'FE' => [
            'backend' => \TYPO3\CMS\Core\Session\Backend\DatabaseSessionBackend::class,
            'options' => [
                'table' => 'fe_sessions',
                'has_anonymous' => true,
            ]
        ]
    ],
],
Copied!

Using Redis to store sessions 

TYPO3 also comes with the possibility to store sessions in a Redis key-value database.

The Redis session storage can be configured with config/system/settings.php in the SYS entry:

A sample configuration will look like this:

'SYS' => [
    'session' => [
        'BE' => [
            'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
            'options' => [
                'hostname' => 'redis.myhost.example',
                'password' => 'passw0rd',
                'database' => 0,
                'port' => 6379
            ]
        ],
        'FE' => [
            'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
            'options' => [
                'hostname' => 'redis.myhost.example',
                'password' => 'passw0rd',
                'database' => 0,
                'port' => 6379
            ]
        ],
    ],
],
Copied!

The available options are:

hostname
Name of the server the redis database service is running on. Default: 127.0.0.1
port
Port number the redis database service is listening to. Default: 6379
database
The redis database number to use. Default: 0
password
The password to use when connecting to the specified database. Optional.

Writing your own session storage 

Custom sessions storage backends can be created by implementing the interface \TYPO3\CMS\Core\Session\Backend\SessionBackendInterface . The doc blocks in the interface describe how the implementing class must behave. Any number of options can be passed to the session backend.

A custom session storage backend can be used like this (similarly to the Redis backend):

'SYS' => [
    'session' => [
        'FE' => [
            'backend' => \Vendor\Sessions\MyCustomSessionBackend::class,
            'options' => [
                'foo' => 'bar',
            ]
        ],
    ],
],
Copied!

SessionManager API 

class SessionManager
Fully qualified name
\TYPO3\CMS\Core\Session\SessionManager

Example Configuration

$GLOBALS['TYPO3_CONF_VARS']['SYS']['session'] => [
    'BE' => [
        'backend' => \TYPO3\CMS\Core\Session\Backend\FileSessionBackend::class,
        'savePath' => '/var/www/t3sessionframework/data/'
    ],
];
Copied!
getSessionBackend ( string $identifier)

Gets the currently running session backend for the given context

param $identifier

the identifier

Returns
\TYPO3\CMS\Core\Session\Backend\SessionBackendInterface
invalidateAllSessionsByUserId ( \TYPO3\CMS\Core\Session\Backend\SessionBackendInterface $backend, int $userId, ?\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication $userAuthentication = NULL)

Removes all sessions for a specific user ID

param $backend

see constants

param $userId

the userId

param $userAuthentication

the userAuthentication, default: NULL

References 

Authentication services 

The TYPO3 Core uses Services for the authentication process. This family of services (of type "auth") are the only Core usage that consumes the Services API.

The aim of this chapter is to describe the authentication services so that developers feel confident about writing their own.

Why Use Services? 

Services provide the flexibility needed for such a complex process of authentication, where many methods may be desirable (single sign-on, IP-based authentication, third-party servers such as LDAP, etc.) depending on the context.

The ease with which such services can be developed is a strong point in favor of TYPO3, especially in corporate environments.

Being able to toy with priority and quality allows for precise fine-tuning of the authentication chain.

Alternative services are available in the TYPO3 Extension Repository. It is thus possible to find solutions for using LDAP as an authentication server, for example.

You can check which authentication services are installed using the System > Reports > Installed Services view:

All installed authentication services and their priority

The Authentication Process 

The authentication process is not managed entirely by services. It is handled essentially by class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication for the backend (BE) and by class \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication for the frontend (FE), which both inherit from class \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication . The objects for these classes are available via $GLOBALS['BE_USER'] for BackendUserAuthentication and "frontend.user" request attribute for FrontendUserAuthentication.

These classes are called by the bootstrapping process. They manage the workflow of the authentication process. Services are used strictly to identify and validate users based on whatever form of credentials a given service relies on (by default, a username and a password).

The authentication process kicks in on every page request, be it in the FE or the BE. However if a valid session already exists, that session is kept. Strictly speaking, no authentication is performed in such a case.

JSON Web Tokens (JWT) are used to transport user session identifiers in be_typo_user and fe_typo_user cookies.

Using JWT's HS256 (HMAC signed based on SHA256) allows to determine whether a session cookie is valid before comparing with server-side stored session data. This enhances the overall performance a bit, since sessions cookies would be checked for every request to TYPO3's backend and frontend.

The session cookies can be pre-validated without querying the database, which can filter invalid requests and might improve overall performance a bit.

As a consequence session tokens are not sent "as is", but are wrapped in a corresponding JWT message, which contains the following payload:

  • identifier reflects the actual session identifier
  • time reflects the time of creating the cookie (RFC 3339 format)

The login data 

There is a typical set of data that is transmitted to authentication service in order to enable them to do their work:

uname
This is the user name. This can be whatever makes sense for the available authentication services. For the default service, this will match data from the "username" column of the "be_users" or "fe_users" table for BE or FE authentication respectively.
uident
This is the password, possibly encrypted.
uident_text
This is the clear text value of the password. If the password is originally submitted in clear text, both "uident" and "uident_text" contain the same value.

Inside an authentication service, this data is available in $this->login.

The "auth" services API 

The services of type "auth" are further divided into subtypes, which correspond to various steps in the authentication process. Most subtypes exist for both FE and BE and are differentiated accordingly.

To each subtype corresponds a part of the "auth" services public API. They are listed below in the order in which they are called during the authentication process.

processLoginDataBE, processLoginDataFE

This subtype performs preprocessing on the submitted login data.

The method to implement is processLoginData(). It receives as argument the login data and the password transmission strategy (which corresponds to the login security level, where only 'normal' can be used. It returns the boolean value true, when the login data has been successfully processed .

It may also return a numerical value equal to 200 or greater, which indicates that no further login data processing should take place (see The service chain).

In particular, this subtype is implemented by the TYPO3 Core AuthenticationService, which trims the given login data.

getUserFE, getUserBE
This subtype corresponds to the operation of searching in the database if the credentials that were given correspond to an existing user. The method to implement is getUser(). It is expected to return an array containing the user information or false if no user was found.
authUserFE, authUserBE
This subtype performs the actual authentication based on the provided credentials. The method to implement is authUser(). It receives the user information (as returned by getUser()) as an input and is expected to return a numerical value, which is described later.

The service chain 

No matter what subtype, authentication services are always called in a chain. This means that all registered "auth" services will be called, in order of decreasing priority and quality.

However, for some subtypes, there are ways to stop the chain.

For "processLoginDataBE" and "processLoginDataFE" subtypes, the processLoginData() method may return a numerical value of 200 or more. In such a case no further services are called and login data is not further processed. This makes it possible for a service to perform a form of final transformation on the login data.

For "authUserFE" and "authUserBE" subtypes, the authUser() method may return different values:

  • a negative value or 0 (<=0) indicates that the authentication has definitely failed and that no other "auth" service should be called up.
  • a value larger than 0 and smaller than 100 indicates that the authentication was successful, but that further services should also perform their own authentication.
  • a value of 100 or more (>= 100) indicates that the user was not authenticated, this service is not responsible for the authentication and that further services should authenticate.
  • a value of 200 or more (>=200) indicates that the authentication was successful and that no further tries should be made by other services down the chain.
auth failed auth success no auth
continue   1..99 100..199
stop <= 0 >= 200  

For "getUserFE" and "getUserBE" subtypes, the logic is reversed. The service chain will stop as soon as one user is found.

Developing an authentication service 

Use the Service API to implement your service class. When developing your own "auth" services, the chances are high that you will want to implement only the "getUser*" and "authUser*" subtypes.

There are several public extensions providing such services, so you should be able to find examples to inspire and guide you. Anyway authentication services can be very different from one another, so it wouldn't make much sense to try and provide an example in this manual.

One important thing to know is that the TYPO3 authentication process needs to have users inside database records ("fe_users" or "be_users"). This means that if you interface with a third-party server, you will need to create records on the TYPO3 side. It is up to you to choose whether this process happens on the fly (during authentication) or if you want to create an import process (as a Scheduler task, for example) that will synchronize users between TYPO3 and the remote system.

For the authUser() method, you will want to take care about the return values. If your service should be the final authority for authentication, it should not only have a high priority, but also return values which stop the service chain (i.e. a negative value for failed authentication, 200 or more for a successful one). On the other hand, if your service is an alternative authentication, but should fall back on TYPO3 if unavailable, you will want to return 100 on failure, so that the default service can take over.

Things can get a bit hairy if you have a scenario with mixed sources, for example some users come from a third-party server but others exist only in TYPO3. In such a case, you want to make sure that your service returns definite authentication failures only for those users which depend on the remote system and let the default authentication proceed for "local" TYPO3 users.

Advanced Options 

There are some special configuration options which can be used to modify the behaviour of the authentication process. Some impact the inner working of the services themselves, others influence when services are called.

It is possible to force TYPO3 to go through the authentication process for every request no matter any existing session. By setting the following local configuration either for the FE or the BE:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['BE_alwaysFetchUser'] = true;
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['BE_alwaysAuthUser'] = true;

$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['FE_alwaysFetchUser'] = true;
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['FE_alwaysAuthUser'] = true;
Copied!

the authentication process will be fully run on each request. Both flags may not be necessary depending on what your service does exactly.

A more fine-grained approach allows for triggering the authentication process only when a valid session does not yet exist. The settings are:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['BE_fetchUserIfNoSession'] = true;
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['FE_fetchUserIfNoSession'] = true;
Copied!

The authentication process can also be forced to go through all services for the "getUser*" subtype by setting:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['BE_fetchAllUsers'] = true;
$GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']['setup']['FE_fetchAllUsers'] = true;
Copied!

for BE or FE respectively. This will collect all possible users rather than stopping at the first one available.

CSRF-like request token handling 

New in version 12.0

A CSRF-like request token handling is available to mitigate potential cross-site requests on actions with side effects. This approach does not require an existing server-side user session, but uses a nonce as a "pre-session". The main scope is to ensure a user actually has visited a page, before submitting data to the webserver.

This token can only be used for HTTP methods POST, PUT or PATCH, but for instance not for GET request.

The \TYPO3\CMS\Core\Middleware\RequestTokenMiddleware resolves request tokens and nonce values from a request and enhances responses with a nonce value in case the underlying application issues one. Both items are serialized as a JSON Web Token (JWT) hash signed with HS256. Request tokens use the provided nonce value during signing.

Session cookie names involved for providing the nonce value:

  • typo3nonce_[hash] in case request served with plain HTTP
  • __Secure-typo3nonce_[hash] in case request served with secured HTTPS

Submitting request token value to application:

  • HTTP body, for example in <form> via parameter __RequestToken
  • HTTP header, for example in XHR via header X-TYPO3-Request-Token

Workflow 

The sequence looks like the following:

  1. Retrieve nonce and request token values

    This happens on the previous legitimate visit on a page that offers a corresponding form that shall be protected. The RequestToken and Nonce objects (later created implicitly in this example) are organized in the \TYPO3\CMS\Core\Context\SecurityAspect .

    Changed in version 13.3

    EXT:my_extension/Classes/Controller/MyController.php
    <?php
    
    use TYPO3\CMS\Core\Security\RequestToken;
    use TYPO3\CMS\Core\View\ViewFactoryData;
    use TYPO3\CMS\Core\View\ViewFactoryInterface;
    
    final class MyController
    {
        public function __construct(
            private readonly ViewFactoryInterface $viewFactory,
        ) {}
    
        public function showFormAction()
        {
            $view = $this->viewFactory->create(new ViewFactoryData(/* ... */));
            // creating new request token with scope 'my/process' and hand over to view
            $requestToken = RequestToken::create('my/process');
            $view->assign('requestToken', $requestToken);
            // ...
        }
    
        public function processAction()
        {
            // for the implementation, see below
        }
    }
    
    Copied!
    EXT:my_extension/Resources/Private/Templates/ShowForm.html
    <!-- Assign request token object for ViewHelper -->
    <f:form action="process" requestToken="{requestToken}">
        ...
    </f:form>
    Copied!

    The HTTP response on calling the shown controller action above will be like this:

    HTTP/1.1 200 OK
    Content-Type: text/html; charset=utf-8
    Set-Cookie: typo3nonce_[hash]=[nonce-as-jwt]; path=/; httponly; samesite=strict
    
    ...
    <form action="/my/process" method="post">
        ...
        <input type="hidden" name="__request_token" value="[request-token-as-jwt]">
        ...
    </form>
    Copied!
  2. Invoke action request and provide nonce and request token values

    When submitting the form and invoking the corresponding action, same-site cookies typo3nonce_[hash] and request-token value __RequestToken are sent back to the server. Without using a separate nonce in a scope that is protected by the client, the corresponding request token could be easily extracted from markup and used without having the possibility to verify the procedural integrity.

    The middleware \TYPO3\CMS\Core\Middleware\RequestTokenMiddleware takes care of providing the received nonce and received request token values in \TYPO3\CMS\Core\Context\SecurityAspect . The handling controller action needs to verify that the request token has the expected 'my/process' scope.

    EXT:my_extension/Classes/Controller/MyController.php
    <?php
    
    use TYPO3\CMS\Core\Context\Context;
    use TYPO3\CMS\Core\Context\SecurityAspect;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    
    final class MyController
    {
        public function showFormAction()
        {
            // for the implementation, see above
        }
    
        public function processAction()
        {
            $context = GeneralUtility::makeInstance(Context::class);
            $securityAspect = SecurityAspect::provideIn($context);
            $requestToken = $securityAspect->getReceivedRequestToken();
    
            if ($requestToken === null) {
                // No request token was provided in the request
                // for example, (overridden) templates need to be adjusted
            } elseif ($requestToken === false) {
                // There was a request token, which could not be verified with the nonce
                // for example, when nonce cookie has been overridden by another HTTP request
            } elseif ($requestToken->scope !== 'my/process') {
                // There was a request token, but for a different scope
                // for example, when a form with different scope was submitted
            } else {
                // The request token was valid and for the expected scope
                $this->doTheMagic();
                // The middleware takes care to remove the cookie in case no other
                // nonce value shall be emitted during the current HTTP request
                if ($requestToken->getSigningSecretIdentifier() !== null) {
                    $securityAspect->getSigningSecretResolver()->revokeIdentifier(
                        $requestToken->getSigningSecretIdentifier(),
                    );
                }
            }
        }
    }
    
    Copied!

Password policies 

New in version 12.0

Introduction 

TYPO3 includes a password policy validator which can be used to validate passwords against configurable password policies. A default password policy is included which ensures that passwords meet the following requirements:

  • At least 8 characters
  • At least one number
  • At least one upper case character
  • At least one special character
  • It must be different than current password (if available)

Password policies can be configured individually for both frontend and backend context. It is also possible to extend a password policy with custom validation requirements.

The password policy applies to:

  • Creating a backend user during installation
  • Setting a new password for a backend user in User settings
  • Resetting a password for a backend user
  • Resetting a password for a frontend user
  • Password fields in tables be_users and fe_users

Optionally, a password policy can be configured for custom TCA fields of the type password.

Configuring password policies 

A password policy is defined in the TYPO3 global configuration. Each policy must have a unique identifier (the identifier default is reserved by TYPO3) and must at least contain one validator.

The password policy identifier is used to assign the defined password policy to the backend and/or frontend context. By default, TYPO3 uses the password policy default:

config/system/settings.php | typo3conf/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = 'default';
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = 'default';
Copied!

A custom password policy with the identifier simple can be configured like:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['simple'] = [
    'validators' => [
        \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator::class => [
            'options' => [
                'minimumLength' => 6,
            ],
        ],
    ],
];
Copied!

Then assign the custom password policy simple to frontend and/or backend context:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = 'simple';
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = 'simple';
Copied!

Password policy validators 

TYPO3 ships with two password policy validators, which are both used in the default password policy.

\TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator  

This validator has the ability to ensure a complex password with a defined minimum length and four individual requirements.

The following options are available:

minimumLength

minimumLength
type

int

Default

8

The minimum length of a given password.

upperCaseCharacterRequired

upperCaseCharacterRequired
type

bool

Default

true

If set to true at least one upper case character (A-Z) is required.

lowerCaseCharacterRequired

lowerCaseCharacterRequired
type

bool

Default

true

If set to true at least one lower case character (a-z) is required.

digitCharacterRequired

digitCharacterRequired
type

bool

Default

true

If set to true at least one digit character (0-9) is required.

specialCharacterRequired

specialCharacterRequired
type

bool

Default

true

If set to true at least one special character (not 0-9, a-z, A-Z) is required.

\TYPO3\CMS\Core\PasswordPolicy\Validator\NotCurrentPasswordValidator  

This validator can be used to ensure, that the new user password is not equal to the old password. The validator must always be configured with the exclude action \TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction::NEW_USER_PASSWORD, because it should be excluded, when a new user account is created.

Third-party validators 

The extension EXT:add_pwd_policy provides additional validators.

Disable password policies globally 

To disable the password policy globally (e.g. for local development) an empty string has to be supplied as password policy for frontend and backend context:

config/system/additional.php | typo3conf/system/additional.php
if (\TYPO3\CMS\Core\Core\Environment::getContext()->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = '';
    $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = '';
}
Copied!

Custom password validator 

To create a custom password validator, a new class has to be added which extends \TYPO3\CMS\Core\PasswordPolicy\Validator\AbstractPasswordValidator . It is required to overwrite the following functions:

  • public function initializeRequirements(): void
  • public function validate(string $password, ?ContextData $contextData = null): bool

Please refer to \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator for a detailed implementation example.

Validate a password manually 

You can use the \TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator to validate a password using the validators configured in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .

The class cannot be injected as it must be instantiated with an action. Available actions can be found in enum EXT:core/Classes/PasswordPolicy/PasswordPolicyAction.php (GitHub).

Example:

In the following example a command to generate a public-private key pair validates the password from user input against the default policy of the current TYPO3 installation.

EXT:my_extension/Classes/Command/PrivateKeyGeneratorCommand.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\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;

#[AsCommand(
    name: 'myextension:generateprivatekey',
    description: 'Generates an encrypted private key',
)]
final class PrivateKeyGeneratorCommand extends Command
{
    // Implement class MyService
    public function __construct(private readonly MyService $myService)
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $passwort = (string)$io->askHidden(
            'Please enter the password to encrypt the key',
        );
        $passwordValidator = new PasswordPolicyValidator(PasswordPolicyAction::NEW_USER_PASSWORD);
        $result = $passwordValidator->isValidPassword($passwort);
        if ($result === true) {
            $this->myService->generatePrivateKey($passwort);
            return Command::SUCCESS;
        }
        $io->error('The password must adhere to the default password policy.');
        return Command::FAILURE;
    }
}
Copied!

Event 

The following PSR-14 event is available:

Multi-factor authentication 

Introduction 

TYPO3 is capable of authentication via multiple factors, in short "multi-factor authentication" or "MFA". This is sometimes also referred to "2FA" as a 2-factor authentication process, where - in order to log in - the user needs

  1. "something to know" (= the password) and
  2. "something to own" (= an authenticator device, or an authenticator app on mobile phones or desktop devices).

Read more about the concepts of MFA here: https://en.wikipedia.org/wiki/Multi-factor_authentication

TYPO3 Login Screen for entering MFA code (TOTP)

TYPO3 ships with some built-in MFA providers by default. But more importantly, TYPO3 provides an API to allow extension authors to integrate their own MFA providers.

The API is designed in a way to allow providers to be used for TYPO3 backend authentication or frontend authentication with a multi-factor step in-between.

Managing MFA providers is currently possible via the User Settings module in the tab called Account security.

Manage your MFA providers in the User Settings module

The Account security tab displays the current state:

  • whether MFA can be configured
  • whether MFA is activated or
  • whether some MFA providers are locked

Included MFA providers 

TYPO3 Core includes two MFA providers:

Time-based one-time password (TOTP) 

TOTP is the most common MFA implementation. A QR code is scanned (or alternatively, a shared secret can be entered) to connect an authenticator app such as Google Authenticator, Microsoft Authenticator, 1Password, Authly, or others to the system and then synchronize a token, which changes every 30 seconds.

On each log-in, after successfully entering the password, the six-digit code shown by the authenticator app must be entered.

Recovery codes 

This is a special provider which can only be activated, if at least one other provider is active. It is only meant as a fallback provider, in case the authentication credentials for the "main" provider(s) are lost. It is encouraged to activate this provider, and keep the codes at a safe place.

Select a MFA provider screen

Third-party MFA providers 

Some third-party MFA providers are available:

Setting up MFA for a backend user 

Each provider is displayed with its icon, the name and a short description in the MFA configuration module. In case a provider is active, this is indicated by a corresponding label, next to the provider's title. The same goes for a locked provider - an active provider, which can currently not be used since the provider-specific implementation detected some unusual behaviour, for example, too many false authentication attempts. Additionally, the configured default provider indicates this state with a "star" icon, next to the provider's title.

Each inactive provider contains a Setup button which opens the corresponding configuration view. This view can be different depending on the MFA provider.

MFA TOTP provider configuration screen

Each provider contains an Edit/Change button, which allows to adjust the provider's settings. This view allows, for example, to set a provider as the default (primary) provider, to be used on authentication.

In case the provider is locked, the Edit/Change button changes its button title to Unlock. This button can be used to unlock the provider. This, depending on the provider to unlock, may require further actions by the user.

The Deactivate button can be used to deactivate the provider. Depending on the provider, this will usually completely remove all provider-specific settings.

The "Authentication view" is displayed as soon as a user with at least one active provider has successfully passed the username and password mask.

As for the other views, it is up to the specific provider, used for the current multi-factor authentication attempt, what content is displayed in which view. If the user has further active providers, the view displays them as "Alternative providers" in the footer to allow the user to switch between all activated providers on every authentication attempt.

All providers need to define a locking functionality. In case of the TOTP and recovery code providers, this, for example, includes an attempts count. These providers are locked in case a wrong OTP was entered three times in a row. The count is automatically reset as soon as a correct OTP is entered or the user unlocks the provider in the backend.

All TYPO3 Core providers also feature the "Last used" and "Last updated" information which can be retrieved in the "Edit/Change" view.

By default, the field in the User Settings module is displayed for every backend user. It is possible to disable it for specific users via user TSconfig:

setup.fields.mfaProviders.disabled = 1
Copied!

Administration of user's MFA providers 

If a user is not able to access the backend anymore, for example, because all of their active providers are locked, MFA needs to be disabled by an administrator for this specific user.

Administrators are able to manage the user's MFA providers in the corresponding user record. The new Multi-factor authentication field displays a list of active providers and a button to deactivate MFA for the user, or only a specific MFA provider.

The listing of backend users in the System > Backend Users module also displays for each user, whether MFA is enabled or currently locked. This allows an administrator to analyze the MFA usage of their users at a glance.

The System > Configuration admininistration module shows an overview of all currently registered providers in the installation. This is especially helpful to find out the exact provider identifier, needed for some user TSconfig options.

MFA providers in the configuration module

Configuration 

Enforcing MFA for users 

It seems reasonable to require MFA for specific users or user groups. This can be achieved with $GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa'] which allows four options:

0
Do not require multi-factor authentication (default)
1
Require multi-factor authentication for all users
2
Require multi-factor authentication only for non-admin users
3
Require multi-factor authentication only for admin users

To set this requirement only for a specific user or user group, a user TSconfig option auth.mfa.required <t3tsref:user-auth-mfa-required> is available. The user TSconfig option overrules the global configuration.

auth.mfa.required = 1
Copied!

Allowed provider 

It is possible to only allow a subset of the available providers for some users or user groups.

A configuration option "Allowed multi-factor authentication providers" is available in the user groups record in the "Access List" tab.

There may be use cases in which a single provider should be disallowed for a specific user, which is configured to be allowed in one of the assigned user groups. Therefore, the user TSconfig option auth.mfa.disableProviders can be used. It overrules the configuration from the "Access List": if a provider is allowed in "Access List" but disallowed via user TSconfig, it will be disallowed for the user or user group the TSconfig applies to.

This does not affect the remaining allowed providers from the "Access List".

auth.mfa.disableProviders := addToList(totp)
Copied!

TYPO3 integration and API 

To register a custom MFA provider, the provider class has to implement the EXT:core/Classes/Authentication/Mfa/MfaProviderInterface.php (GitHub), shipped via a third-party extension. The provider then has to be configured in the extension's Services.yaml or Services.php file with the mfa.provider tag.

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\Authentication\Mfa\MyProvider:
    tags:
      - name: mfa.provider
        identifier: 'my-provider'
        title: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider.title'
        description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider.description'
        setupInstructions: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider.setupInstructions'
        icon: 'tx-myextension-provider-icon'
Copied!

Read how to configure dependency injection in extensions.

This will register the provider MyProvider with the my-provider identifier. To change the position of your provider the before and after arguments can be useful. This can be needed, for example, if you like your provider to show up prior to any other provider in the MFA configuration module. The ordering is also taken into account in the authentication step while logging in. Note that the user-defined default provider will always take precedence.

If you do not want your provider to be selectable as a default provider, set the defaultProviderAllowed argument to false.

You can also completely deactivate existing providers with:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider: ~
Copied!

You can also register multiple providers:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\Authentication\Mfa\MyFirstProvider:
    tags:
      - name: mfa.provider
        identifier: 'my-provider-1'
        title: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.title'
        description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.description'
        setupInstructions: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider1.setupInstructions'
        icon: 'tx-myextension-provider1-icon'

  MyVendor\MyExtension\Authentication\Mfa\MySecondProvider:
    class: TYPO3\CMS\Core\Authentication\Mfa\Provider\TotpProvider
    tags:
      - name: mfa.provider
        identifier: 'my-provider-2'
        title: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.title'
        description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.description'
        setupInstructions: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myProvider2.setupInstructions'
        icon: 'tx-myextension-provider2-icon'
        # Important so that this provider acts as a fallback
        defaultProviderAllowed: true
        before: 'recovery-codes'
        # Execute after the primary totp
        after: 'totp'
Copied!

The MfaProviderInterface contains a lot of methods to be implemented by the providers. This can be split up into state-providing ones, for example, isActive() or isLocked(), and functional ones, for example, activate() or update().

Their exact task is explained in the corresponding PHPDoc of the interface files and the Core MFA provider implementations.

All of these methods are receiving either the current PSR-7 request object, the \TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager or both. The MfaProviderPropertyManager can be used to retrieve and update the provider-specific properties and also contains the getUser() method, providing the current user object.

To store provider-specific data, the MFA API uses a new database field mfa, which can be freely used by the providers. The field contains a JSON-encoded array with the identifier of each provider as array key. Common properties of such provider array could be active or lastUsed. Since the information is stored in either the be_users or the fe_users table, the context is implicit. Same goes for the user the providers deal with. It is important to have such a generic field so providers are able to store arbitrary data, TYPO3 does not need to know about.

To retrieve and update the providers data, the already mentioned MfaProviderPropertyManager, which is automatically passed to all necessary provider methods, should be used. It is highly discouraged to directly access the mfa database field.

Autoloading 

The class autoloader takes care of finding classes in TYPO3.

About makeInstance() 

\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() is a generic way throughout Core and extensions to create objects. It takes care of singleton and XCLASS handling.

A developer can instantiate classes using makeInstance() - if dependency injection cannot be used. There are some situations where new is used over makeInstance(), effectively dropping especially the direct ability to XCLASS:

  • Data transfer objects are often created with new. A good example are PSR-14 events: The calling class creates a data transfer object that is hand over to the consumer. These DTOs must never be changed by an extension, since they are a contract both caller and consumer must stick to. They are thus created using new to prevent XCLASSing.
  • Structures with a dedicated API that allows own implementations on a configuration level sometimes do not use makeInstance: Many Core constructs come with an API to allow custom classes by dedicated configuration. Those implement a factory pattern to deal with this. An example is the PSR-15 middleware stack.

Autoloading classes 

There is one autoloader used, the one of Composer. No matter if you run TYPO3 in Composer mode or not (Classic mode), TYPO3 uses the Composer autoloader to resolve all class file locations.

Loading classes with Composer mode 

In Composer mode, the autoloader checks for (classmap and PSR-4) autoloading information inside your extension's composer.json. If you do not provide this, the autoloader falls back to the classmap autoloading like in non-Composer mode.

Troubleshooting 

  • Dump the class loading information manually via

    composer dumpautoload
    Copied!

    and check that the autoload information is updated. Typically you would check vendor/composer/ to hold files like autoload_classmap.php and autoload_psr4.php, etc..

  • Check if you find the required class names in these files and install any missing extensions.

Example:

$ tree vendor/composer
.
├── ClassLoader.php
├── LICENSE
├── autoload_classmap.php
├── autoload_files.php
├── autoload_namespaces.php
├── autoload_psr4.php
├── autoload_real.php
├── autoload_static.php
├── include_paths.php
└── installed.json
Copied!

Loading classes without Composer mode 

This means, you did not install TYPO3 via a require statement inside your composer.json. It's a regular old-school install where the TYPO3 source and the symlinks (typo3/index.php) are setup manually.

In this case, every time you install an extension, the autoloader scans the whole extension directory for classes. No matter if they follow any convention at all. There is just one rule: put each class into its own file. This also means that there can only be a single class per file.

You can also explicitly configure autoloading in the ext_emconf.php.

The generated typo3conf/autoload_classmap.php is a large array with a mapping of classnames to their location on the disk:

typo3conf/autoload_classmap.php
<?php

// autoload_classmap.php @generated by TYPO3

$typo3InstallDir = \TYPO3\CMS\Core\Core\Environment::getPublicPath();

return array(
    'Schnitzler\\Templavoila\\Clipboard\\Clipboard' => $typo3InstallDir . 'typo3conf/ext/templavoila/Classes/Clipboard/Clipboard.php',
    'tx_templavoila_pi1' => $typo3InstallDir . 'typo3conf/ext/templavoila/Compatibility/class.tx_templavoila_pi1.php',
    ...
);
Copied!

This method is failsafe unless the autoload information cannot be written. In this case, check the Install Tool for warnings and make sure that typo3temp/ is writable.

Troubleshooting: 

If your classes cannot be found, try the following approaches.

  • Dump the class loading information manually with the following command:

    php typo3/sysext/core/bin/typo3 dumpautoload
    Copied!
  • If that command itself fails, please (manually) uninstall the extension and try reinstalling it (via the Extension Manager).
  • If you are still not lucky, the issue is definitely on your side and you should double check the write permissions on typo3temp.

Best practices 

  • If you didn't do so before, have a look at the PSR-4 standard. It defines very good rules for naming classes and the files they reside in. Really, read the specs and start using PSR-4 in your projects. It's unlikely that there will be any other more advanced standard in the near future in the PHP world. PSR-4 is the way to go and you should embrace it.
  • Even if you do not use Composer mode and the class mapping of the autoloader allows you to use whatever you want, stick to PSR-4. It's not only a very good standard to find classes, but it will also help organizing your code.
  • PSR-4 is all about namespaces. No matter if you like namespaces or not, use them. Namespaces exist since PHP 5.3, so you will be able to use them in any modern TYPO3 project due to the minimum PHP requirements of TYPO3 itself.

Further reading 

ComposerClassLoader 

Integrating Composer class loader into TYPO3 

In our efforts to make TYPO3 faster and closer oriented to common PHP standard systems, we looked into the integration of the class loader that is used by all Composer-based projects. We consider this functionality a crucial feature for the future of TYPO3 on the base level, but also as a dramatic increase of the overall performance of every request inside TYPO3.

Understanding the TYPO3 class loader 

The TYPO3 class loader is instantiated within the TYPO3 Bootstrap at a very early point. It does a lot of logic (checking ext_autoload.php, ClassAliasMap.php), and caches this logic away on a per-class basis by default in typo3temp/Cache/ to store all information for a class. This information contains: The full path to the class, the namespaced class name itself and possible class aliases.

The latter part looks into all extensions and checks the Migrations/ClassAliasMap.php file for any possible “legacy class” that could be used (e.g. t3lib_extmgm). This way, all extensions still using non-namespaced class that are shipped with the TYPO3 core are still made available.

The information is stored in a SimpleFileBackend via the built-in Caching Framework by default. At the early stage of the bootstrap process some classes need to be included manually as the whole TYPO3 core engine has not been loaded yet. This is done for all PHP classes in use, which may result in 500+ files inside typo3temp/Cache which are created one by one on an initial request with no caches set up. This is done by intention on a per-file-basis during runtime as a cache file is only created if a PHP class is requested to be instantiated. On a second hit, the caching framework does not create the cache files, but fetches one by one for each class used in the request via a separate file_get_contents() call.

When debugging TYPO3 on an initial request, there are a lot of file_get_contents() and file_put_contents() calls to store and fetch this information. This is quite a lot of overhead for loading PHP classes. Even without a lot of class aliases (e.g. in CMS7) this overhead of writing / storing the file caches still exists. Some overhead however is already taken care, especially if a class is loaded which is shipped with the core and no class alias is used.

This is all built in a way so a lot of backwards-compatibility can be ensured.

Understanding the Composer class loader 

Compared to the TYPO3 class loader, the Composer class loader concept differs in the following major points:

Caching on build stage 

When setting up a project, like a TYPO3 project, Composer checks the main composer.json of a project and builds a static file with all PSR-4 prefixes defined in that json file. Unless in a development environment or when updating the source, this file does not need to be rebuilt as the PHP classes of the loaded packages won’t change in a regular instance. This way all classes available inside TYPO3 are always available to the class loader.

Using PSR-4 compatible prefix-based resolving 

Instead of looking up every single class and caching the information away, Composer works on a “prefix”-based resolution. As an example, the Composer class loader only needs to know that all PHP classes starting with \TYPO3\CMS\Core are located within EXT:core/Classes. The rest is done by a simple resolution to include the necessary PHP class files. This means that the information to be cached away is only the list of available namespace prefixes.

The definition of these prefixes is set inside the composer.json file of each package or distribution / project.

Autoloading developer-specific data differently 

The Composer class loader checks the composer.json for a development installation differently, including for example unit and functional tests separately to the rest of the installation. The static map with all namespaces are thus different when using Composer with composer install or composer install --no-dev.

Integration Approach 

The Composer class loader is injected inside the Bootstrap process of TYPO3 and registered before the TYPO3 class loader. This means that a lookup on a class name is first checked via the Composer logic, and if none found, the regular TYPO3 class loader takes over.

The support for class aliases is quite important for TYPO3, but is not supported by Composer by default. There is a separate Composer package created by Helmut Hummel (available on GitHub) which serves as a facade to the Composer class loader and creates not just the information for the prefixes but also the available class aliases for a class and loads them as well.

The necessary information about the “which namespaced classes can be found at which place” is created before every release and shipped inside the typo3_src directory. The generated class information is available under typo3/contrib/vendor/composer/. For TYPO3 installations that are set up with composer, the TYPO3 bootstrap checks Packages/Libraries/autoload.php first which can be shipped with any Composer-based project and include many more PHP Composer packages than just TYPO3 extensions. To ensure maximum backwards-compatibility, the option to load from Packages/Library/autoload.php instead of the shipped "required-core-packages-only" needs to be activated via an environment variable called TYPO3_COMPOSER_AUTOLOAD which needs to be set on server-side level.

If the Composer-based logic is not used in some legacy cases (for extensions etc), the usual TYPO3 class loader comes into play and does the same logic as before.

Project setup and extension considerations 

If you already use Composer to set up your project, and the composer.json and their extensions ship a valid composer.json, the Composer class loader generates the valid PSR-4 cache file with all prefixes on installation and update. Running "composer update" will automatically re-generate the PSR-4 cache file.

The Composer class loader also supports PSR-0 and static inclusion of files, which can be used as well.

As a base line: Any regular installation will see a proper speed improvement after the update to the Composer class loader.

Backend APIs 

The following APIs are of interest if you want to configure or extend the functionalities of the backend.

Contents:

Users and groups 

TYPO3 features an access control system based on users and groups.

Users 

Each user of the backend must be represented with a single record in the table "be_users". This record contains the username and password, other meta data and some permissions settings.

Part of the editing form for user "simple_editor" of the Introduction Package

The above screenshot shows a part of the editing form for the backend user "simple_editor" from the Introduction Package. If you have an Introduction Package available, you can check further properties of that user. It is part of the "Simple editors" group, has a name, an email address and its default language for the backend is English.

It is possible to assign rights directly to a user, but it is much better done using groups. Furthermore groups offer far more options.

Groups 

Each user can also be a member of one or more groups (from the "be_groups" table) and each group can include sub-groups. Groups contain the main permission settings you can set for a user. Many users can be a member of the same group and thus share permissions.

When a user is a member of many groups (including sub-groups) then the permission settings are added together so that the more groups a user is a member of, the more access is granted to him.

Part of the editing form for group "Simple editors" of the Introduction Package

This screenshot shows just an extract of the group editing form. It contains many more fields!

See Access Control Options for details.

The "admin" user 

There is a special kind of backend users called "Admin". When creating a backend user, just check the "Admin!" box in the "General" tab and that user will become an administrator. There's no need to set further access options for such a user: an admin user can access every single feature of the TYPO3 backend, like the "root" user on a UNIX system.

All systems must have at least one "admin" user and most systems should have only "admin" users for the developers - not for any editor. Make sure to not share TYPO3 accounts with multiple users but create dedicated accounts for everyone. Not even "super users" should be allowed "admin" access since that will most likely grant them access to more than they need.

Admin users are differentiated with an orange icon.

In Web > List view, the different icon for admin users

Location of users and groups 

Since both backend users and backend groups are represented by records in the database, they are edited just as any other record in the system. However backend users and groups are configured to exist only in the root of the page tree where only admin users have access:

Users and groups reside on the root page

Records located in the page tree root are identified by having their "pid" fields set to zero. The "pid" field normally contains the relation to the page where a record belongs. Since no pages can have the id of zero, this is the id of the root. Notice that only "admin" users can edit records in the page root!

If you need non-admin users to create new backend users, have a look at the TYPO3 system extension sys_action for a possible solution.

Password reset functionality 

TYPO3 backend users can reset their password if they use the default TYPO3 login mechanism.

To display the reset link on the backend login page, the following criteria must be met:

  • The user has a password entered previously (indicating that no third-party login has been used).
  • The user has a valid email address added to their user record.
  • The user is neither deleted nor disabled.
  • The email address is used only once for all backend users of the instance.

Once the user has entered their email address, an email is sent with a link that allows to set a new password, which must consist of at least eight characters. The link is valid for 2 hours and a token is added to the link. If the password is entered correctly, it will be updated for the user and they can log in.

New in version 12.1

The new password that the user specifies must comply with the configured password policy for the backend.

New in version 12.3

The username of the backend user is displayed in the password recovery email alongside the reset link.

New in version 13.0

A new array variable {userData} has been added to the password recovery FluidEmail object. It contains the values of all fields belonging to the affected frontend user.

Notes on security 

  • When having multiple users with the same email address, no reset functionality is provided.
  • No information disclosure is built-in, so if the email address is not in the system, it is not disclosed to the outside.
  • Rate limiting is enabled so that three emails can be sent per email address within 30 minutes.
  • Tokens are stored in the database but hashed again just like the password.
  • When a user has logged in successfully (for example, because they remembered the password), the token is removed from the database, effectively invalidating all existing email links.

Implications of displaying the username in the email 

  • A third-party gaining access to the email account has all information needed to log in into the TYPO3 backend and potentially cause damage to the website.
  • Without the username a third-party could only reset the password of the TYPO3 backend user, but not log in, if the username is different from the email address.
  • It is also possible to override the ResetRequested email templates to remove the username and customize the result.
  • It is highly recommend to protect backend accounts using Multi-factor authentication.

Global configuration 

The feature is enabled by default and can be deactivated entirely via the system-wide configuration option:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordReset'] = false;
Copied!

Optionally, it is possible to restrict this feature to non-admins only by setting the following system-wide option to false.

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins'] = false;
Copied!

Both options can be configured in the Admin Tools > Settings module or in the Install Tool, but can also be set manually via config/system/settings.php or config/system/additional.php.

Reset password for user 

Administrators can reset a user's password. This is useful primarily for security reasons, so that an administrator does not have to send a password over to a user in plain text (for example, by email).

The administrator can use the CLI command:

vendor/bin/typo3 backend:resetpassword https://example.com/typo3/ editor@example.com
Copied!
typo3/sysext/core/bin/typo3 backend:resetpassword https://example.com/typo3/ editor@example.com
Copied!

where usage is described like this:

backend:resetpassword <backend_url> <email_address>
Copied!

Roles 

Another popular approach to setting up users is roles. This concept is basically about identifying certain roles that users can take and then allow for a very easy application of these roles to users.

TYPO3 access control is far more flexible and allows for such detailed configuration that it lies very far from the simple and straight forward concept of roles. This is necessary as the foundation of a system like TYPO3 should fit many possible usages.

However it is perfectly possible to create groups that act like "roles". This is what you should do:

  1. Identify the roles you need; Developer, Administrator, Editor, Super User, User, ... etc.
  2. Configure a group for each role: attribute permissions needed to fulfill each role.
  3. Consider having a general group which all other groups include - this would basically configure a shared set of permissions for all users.

Access Control Options 

The permissions of fully initialized backend users are the result of the rights granted in their own user records and all the user groups they belong to.

The permissions are divided into the following conceptual categories:

Access lists
These grant access to backend modules, database tables and fields.
Mounts
Parts of the page tree and server file system.
Page permissions
Access to work on individual pages based on the user id and group ids.
User TSconfig

A flexible and hierarchical configuration structure defined by TypoScript syntax. This typically describes "soft" permission settings and options for the user or group which can be used to customize the backend and individual modules.

All user TSconfig options are described in the TSconfig Reference

Access Lists 

Access lists are defined at group-level. Usage of access lists for defining user rights is described in chapter Setting up user group permissions. The various access lists are described here for reference, with additional technical details, where necessary.

Modules

This is a list of submodules a user may be given access to. Access to a main module is implicit, as soon as a user has access to at least one of its submodules.

Not all submodules appear in this list. It is possible to restrict a submodule to admin users only. This is the case, in particular, for all Admin Tools and System modules, as well as the Web > Template module.

Dashboard widgets

A list of the available dashboard widgets a user may be allowed to use on the dashboard.

Tables for listing

A list of all tables a user may be allowed to read in the backend. Again this in not a list of all tables in the database. Some tables are low level and never appear in the backend at all, even for admin users. Other tables are restricted to admin users and thus do not show up in the access list.

Restricting a table to admin users only is done using the TCA property "adminOnly".

Tables for editing
This is exactly the same list of tables as before, but for granting modification rights.
Page types

TYPO3 CMS defines a number of page types. A user can be restricted to access only some of them.

For a full discussion on page types, please refer to the page types chapter.

Excludefields
When defining column tables in TCA, it is possible to set the "exclude" property to "1". This ensures that the field is hidden to users by default. Access to it must be explicitly granted in this access list.
Explicitly allow/deny field values

When a field offers a list of options to select from, it is possible to tell TYPO3 CMS that access to these options is restricted and should be granted explicitly. Such fields and their values appear here.

The related TCA property is "authMode".

Limit to languages
By default users can edit records regardless of what language they are assigned to. Using this list it is possible to restrict users to working only in selected languages.

When a user is a member of more than one group, the access lists for the groups are "added" together.

Mounts 

TYPO3 CMS natively supports two kinds of hierarchical tree structures: the page tree (typically visible in the Web module) and the folder tree (typically visible in the File module). Each tree is generated based on the mount points configured for the current user. So a page tree is drawn from the DB Mounts which are one or more page ids telling the Core from which "start page" to draw the tree(s). Likewise is the folder tree drawn based on file mounts configured for the user.

DB mounts (page mounts) are set by pointing out the page that should be mounted for the user (at user or group-level):

The DB mounts for group "Editors"

This is what the user will see:

Only selected pages are accessible to the user

File Mounts are a little more difficult to set up, as they involve several steps. First of all, you need to have at least one File Storage. By default, you will always have one, pointing to the fileadmin directory. It is created by TYPO3 CMS upon installation.

A File Storage is essentially defined by a File Driver and the path to which it points.

Next we can create a File Mount record (on the root page), which refers to a File Storage:

A file mount pointing to the "user_upload" directory

When defining a File Mount, you can point to a specific folder within the chosen File Storage. Finally the mount is assigned to a user or group:

The file mount is assigned to the "Editors" group

After a successful configuration, the file mount will appear to the user:

The file tree as visible by the user

DB and File Mounts can be set for both the user and group records. Having more than one DB or File Mount will just result in more than one mount point appearing in the trees. However the backend users records have two flags which determine whether the DB/File Mounts of the groups the user belongs to will be mounted as well! This is the default behaviour. So make sure to unset these flags if users should see only their "private" mount points and not those from their groups:

By default DB and File Mounts from groups are set for member users

"Admin" users do not need mount points. As always, they have access to every part of the installation.

Page Permissions 

Page permissions are designed to work like file permissions on UNIX systems. Each page record has an owner user and group and permission settings for the owner, the group and "everybody". This is summarized here:

  • Every page has an owner, group and everybody-permission
  • The owner and group of a page can be empty. Nothing matches with an empty user/group (except "admin" users).
  • Every page has permissions for owner, group and everybody in these five categories (next to the label is the corresponding value):

    Show (1)
    See/Copy page and the page content.
    Edit page content (16)
    Change/Add/Delete/Move page content.
    Edit page (2)
    Change/Move the page, eg. change title, startdate, hidden flag.
    Delete page (4)
    Delete the page and page content.
    New pages (8)
    Create new pages under the page.

Page permissions are set and viewed with the module System > Permissions module:

The Access module and its overview of page rights and owners

Editing permissions is described in details in chapter Page permissions.

A user must be "admin" or the owner of a page in order to edit its permissions.

When a user creates new pages in TYPO3 CMS they will by default get the creating user as owner. The owner group will be set to the first listed user group configured for the users record (if any). These defaults can be changed through page TSconfig.

User TSconfig 

User TSconfig is a hierarchical configuration structure entered in plain text TypoScript. It can be used by all kinds of applications inside of TYPO3 CMS to retrieve customized settings for users which relates to a certain module or part. The options available are described in the document TSconfig .

Other Options 

This chapter presents a few more, miscellaneous options for backend users and groups.

Backend users 

Default language

This is the language in which the backend will be localized for the user. The users can change the language themselves in the User Settings module.

Fileoperation permissions
This is a complement to the File Mounts and defines exactly which operations the user is allowed to perform on both files and folders.
Access options
A backend user can be disabled (first flag in the "General" tab). A disabled user cannot log into the backend anymore. Furthermore, in the "Access" tab a start and end time can be given, defining a time interval during which the user will be allowed to log into the backend. Authentication before the start time and after the end time will automatically fail.
Lock to domain
This setting constrains the user to use a specific domain for logging into the TYPO3 backend. This is very useful in setups with multiple sites.

Backend Groups 

Disable
Setting this flag will immediately disable the group for all members
Lock to domain
This restricts a group to a given domain. If a user logs in from another domain, that group membership will be ignored.
Hide in lists
This flag will prevent the group from appearing in various listings in TYPO3. This includes modules like System > Access.
Inherit settings from groups (Sub Groups)
Assigns sub-groups to this group. Sub-groups are evaluated before the group including them. If a user is a member of a group which includes one or more sub-groups, the user will also be a member of the sub-groups.

More about file mounts 

File mounts require a little more description of the concepts provided by TYPO3. All files are handled by an application layer called the "File Abstraction Layer" (FAL). You can find more information about the basic concepts of FAL.

The FAL is comprised of the following components:

Drivers
Drivers are what makes it possible to access a given type of media storage. The Core provides a driver to access the local file system. Extensions exist that provide a driver for remote systems, like SFTP or platforms like Amazon S3.
Storages

A file storage uses a driver to connect to a given storage system. It is defined by a path pointing into that storage system. There can be several storages using the same driver and pointing to different "directories". The storage configuration depends on the driver it uses.

Thanks to the storage and its driver, the user is able to browse files from within the TYPO3 backend as if they were stored locally.

File mounts
As discussed before, a file mount is the element which is used to actually give access to users to some directories. A file mount is always related to a storage.

Create a new file mount 

To create a new file mount go to the module File > Filelist and create the folder for the mount if it didn't exist yet. Then open the context menu on that folder and choose New File mount, then give the new file mount a name. The entry point is already set.

It is also possible to create a file mount manually in the List module by creating a record of type Filemount. In this case you have to choose the storage and folder manually.

Paths for local driver storage 

The file storages based on the "local file system" driver have an option for relative or absolute paths.

The paths options for a storage based on the local file system driver

"Relative" means that the given path is relative to the fileadmin/ folder (or whatever other folder was configured using $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']. Absolute paths are full paths starting at the root of the file system (i.e. / on Unix systems).

Absolute paths outside of the web root must be explicitly declared in the global configuration option $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']. Any absolute path that you want to declare in a file storage needs to have its first part match the value of $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] (or of the web root, which can be retrieved with \TYPO3\CMS\Core\Core\Environment::getPublicPath()).

As an example, let's say you want to define two storages, one pointing to /home/foo/bar and one pointing to /home/foo/baz. You could declare $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] to be equal to /home/foo/.

Home directories 

TYPO3 CMS also features the concept of "home directories". These are paths that are automatically mounted if they are present at a path configured in the global configuration. Thus they don't need to have a file mount record representing them - they just need a properly named directory to be present.

The parent directory of user/group home directories is defined by $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'] and $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] respectively. Let's say we define the following:

$GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'] = '1:user_homes/';
Copied!

The first part of the definition (before the colon :) is the id of a file storage. The second part is a path relative to that file storage. Assuming file storage with a uid of "1" is the default one pointing to fileadmin/, the following path needs to exist on the server: /path/to/web/root/fileadmin/user_homes/.

Then a directory needs to exist for each user. Again let's assume that we have a user with a uid of "3" and a username of "editor", either of those paths would have to exist:

  • /path/to/web/root/fileadmin/user_homes/3/
  • /path/to/web/root/fileadmin/user_homes/3_editor/

The second possibility is more explicit, but will break if the username is changed.

The same goes for groups, but only using the uid. Assuming a group called "editors" with a uid of "1", and:

$GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] = '1:groups/';
Copied!

we have to create a directory /path/to/web/root/fileadmin/groups/1/.

Having set up all these properties and folders, the user should see the following when moving to the FILE > Filelist module:

The file list with automatically mounted user and group directories

where only the first mount was explicitly assigned to that user. A different icon visually distinguishes automatic file mounts.

The concept of home directories can be efficiently combined with the TSconfig defaultUploadFolder option, which automatically directs all files uploaded by the user to the given directory.

Backend users module 

The System > Backend users module offers a convenient way of working with backend users and groups. It provides a list of both users and groups. The users list can be searched and filtered.

Comparing Users or Groups 

The Backend users module offers the possibility to compare users. Just add users using the "+ Compare" button and then hit the "Compare user list" button. For example, this is the comparison of the three different editors provided by the Introduction Package:

Comparing users in the Backers users module

The same functionality is available for user groups, including a comparison of their inherited permissions.

Impersonating Users ("Switch to") 

We can impersonate (switch) to a user by clicking the Switch to user action icon:

The button to simulate another user

You will then be logged in as that user (note how the user name is prefixed with "SU" for "Simulated User"). To "switch back", use the "Exit" button (which replaces the usual "Logout" button).

Backend with active simulate user

Backend modules API 

Changed in version 12.0

This chapter describes the API that can be used to create custom backend modules in extensions. See the following chapter for a tutorial on how to create custom backend modules.

Backend GUI 

Describes the graphical user interface structure of a backend module and defines how the different parts are called.

Backend module configuration 

Howto register custom modules provided by extensions.

Toplevel modules 

Lists all toplevel modules available by default and explains how to register custom toplevel modules.

ModuleProviderAPI 

The ModuleProvider API, allows extension authors to work with the registered modules.

BeforeModuleCreationEvent 

The PSR-14 BeforeModuleCreationEvent allows extension authors to manipulate the module configuration before it is used to create and register the module.

Button components 

The menu button bar of a backend module can hold various components.

Override backend templates 

Backend templates can be overridden via page TSconfig. But you should be careful: backend templates are mostly not API and can break on updates.

Tutorial and how to 

Learn how to create a backend module step-by-step.

Modules.php - Backend module configuration 

Changed in version 12.0

Registration of backend modules was changed with version 12. If you are using an older version of TYPO3 please use the version switcher on the top left of this document to go to the respective version.

The configuration of backend modules is placed in the dedicated Configuration/Backend/Modules.php configuration file.

See also the Backend module configuration examples.

Module configuration options 

Name Type
string
string
string
string
array
array
bool
string
string
array of strings or string
string
string
string
bool
array
array
array

parent

parent
Type
string

If the module should be a submodule, the parent identifier, for example web has to be set here. Have a look into the list of available toplevel modules.

Extensions can add additional parent modules, see Toplevel modules.

path

path
Type
string
Default
/module/<mainModule>/<subModule>

Define the path to the default endpoint. The path can be anything, but will fallback to the known /module/<mainModule>/<subModule> pattern, if not set.

access

access
Type
string

Can be user (editor permissions), admin, or systemMaintainer.

workspaces

workspaces
Type
string

Can be * (= always), live or offline. If not set, the value of the parent module - if any - is used.

position

position
Type
array

The module position. Allowed values are top and bottom as well as the key value pairs before => <identifier> and after => <identifier>.

appearance

appearance
Type
array

Allows to define additional appearance options. Currently only appearance.renderInModuleMenu is available.

appearance.renderInModuleMenu

appearance.renderInModuleMenu
Type
bool

If set to false the module is not displayed in the module menu.

iconIdentifier

iconIdentifier
Type
string

The module icon identifier

icon

icon
Type
string

Path to a module icon (Deprecated: Use iconIdentifier instead)

labels

labels
Type
array of strings or string

An array with the following keys:

  • title
  • description
  • shortDescription

The value of each array entry can either be a string containing the static text, or a locallang label reference.

Alternatively define the path of a locallang file reference. A referenced file should contain the following label keys:

  • mlang_tabs_tab (used as module title)
  • mlang_labels_tabdescr (used as module description)
  • mlang_labels_tablabel (used as module short description)

component

component
Type
string
Default
TYPO3/CMS/Backend/Module/Iframe

The view component, responsible for rendering the module.

navigationComponent

navigationComponent
Type
string

Changed in version 13.1

@typo3/backend/page-tree/page-tree-element has been renamed to @typo3/backend/tree/page-tree-element. Using old navigation ID will trigger a PHP deprecation warning.

The module navigation component. The following are provided by the Core:

@typo3/backend/tree/page-tree-element
The page tree as used in the Web module.
@typo3/backend/tree/file-storage-tree-container
The file tree as used in the Filelist module.
Migration
'mymodule' => [
    'parent' => 'web',
    ...
-   'navigationComponent' => '@typo3/backend/page-tree/page-tree-element',
+   'navigationComponent' => '@typo3/backend/tree/page-tree-element',
],
Copied!

navigationComponentId

navigationComponentId
Type
string

The module navigation component (Deprecated: Use navigationComponent)

inheritNavigationComponentFromMainModule

inheritNavigationComponentFromMainModule
Type
bool
Default
true

Whether the module should use the parents navigation component. This option defaults to true and can therefore be used to stop the inheritance for submodules.

moduleData

moduleData
Type
array

All properties of the module data object that may be overridden by GET / POST parameters of the request get their default value defined here.

Example

Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?php

declare(strict_types=1);

return [
    'my_module' => [
        // ...
        'moduleData' => [
            'allowedProperty' => '',
            'anotherAllowedProperty' => true,
        ],
    ],
];
Copied!

aliases

aliases
Type
array

List of identifiers that are aliases to this module. Those are added as route aliases, which allows to use them for building links, for example with the \TYPO3\CMS\Backend\Routing\UriBuilder . Additionally, the aliases can also be used for references in other modules, for example to specify a module's parent.

Examples

Example for a new module identifier:

Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?php

declare(strict_types=1);

return [
    'workspaces_admin' => [
        'parent' => 'web',
        // ...
        // choose the previous name or an alternative name
        'aliases' => ['web_WorkspacesWorkspaces'],
    ],
];
Copied!

Example for a route alias identifier:

Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?php

declare(strict_types=1);

return [
    'file_editcontent' => [
        // ...
        'path' => '/file/editcontent',
        'aliases' => ['file_edit'],
    ],
];
Copied!

routeOptions

routeOptions
Type
array

Generic side information that will be merged with each generated \TYPO3\CMS\Backend\Routing\Route::$options array. This can be used for information, that is not relevant for a module aspect, but more relevant for the routing aspect, for example sudo mode.

Default module configuration options (without Extbase) 

Name Type
array

routes

routes
Type
array

Define the routes to this module. Each route requires at least the target. The _default route is mandatory, except for modules which can fall back to a submodule. The path of the _default route is taken from the top-level configuration. For all other routes, the route identifier is taken as path, if not explicitly defined. Each route can define any controller/action pair and can restrict the allowed HTTP methods:

Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Classes\Controller\AnotherController;
use MyVendor\MyExtension\Classes\Controller\MyModuleController;

return [
    'my_module' => [
        // ...
        'routes' => [
            '_default' => [
                'target' => MyModuleController::class . '::overview',
            ],
            'edit' => [
                'path' => '/edit-me',
                'target' => MyModuleController::class . '::edit',
            ],
            'manage' => [
                'target' => AnotherController::class . '::manage',
                'methods' => ['POST'],
            ],
        ],
    ],
];
Copied!

All subroutes are automatically registered in a \TYPO3\CMS\Core\Routing\RouteCollection . The full syntax for route identifiers is <module_identifier>.<sub_route>, for example, my_module.edit. Therefore, using the \TYPO3\CMS\Backend\Routing\UriBuilder to create a link to such a sub-route might look like this:

\TYPO3\CMS\Backend\Routing\UriBuilder->buildUriFromRoute('my_module.edit');
Copied!

Extbase module configuration options 

Name Type
string
array

extensionName

extensionName
Type
string

The extension name in UpperCamelCase for which the module is registered. If the extension key is my_example_extension the extension name would be MyExampleExtension.

controllerActions

controllerActions
Type
array

Define the controller action pair. The array keys are the controller class names and the values are the actions, which can either be defined as array or comma-separated list:

Excerpt of EXT:my_extension/Configuration/Backend/Modules.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Controller\MyModuleController;

return [
    'web_ExtkeyExample' => [
        //...
        'path' => '/module/web/ExtkeyExample',
        'controllerActions' => [
            MyModuleController::class => [
                'list',
                'detail',
            ],
        ],
    ],
];
Copied!

The modules define explicit routes for each controller/action combination, as long as the enableNamespacedArgumentsForBackend feature toggle is turned off (which is the default). This effectively means human-readable URLs, since the controller/action combinations are no longer defined via query parameters, but are now part of the path.

This leads to the following URLs:

  • https://example.com/typo3/module/web/ExtkeyExample
  • https://example.com/typo3/module/web/ExtkeyExample/MyModule/list
  • https://example.com/typo3/module/web/ExtkeyExample/MyModule/detail

The route identifier of corresponding routes is registered with similar syntax as standard backend modules: <module_identifier>.<controller>_<action>. Above configuration will therefore register the following routes:

  • web_ExtkeyExample
  • web_ExtkeyExample.MyModule_list
  • web_ExtkeyExample.MyModule_detail

Debug the module configuration 

All registered modules are stored as objects in a registry. They can be viewed in the backend in the System > Configuration > Backend Modules module.

Exploring registered Backend Modules in the Configuration module

The ModuleProvider API allows extension authors to work with the registered modules.

Backend modules with sudo mode 

You can configure the sudo mode in your backend module like this:

EXT:my_extension/Configuration/Backend/Modules.php
<?php

use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime;

return [
    'tools_ExtensionmanagerExtensionmanager' => [
        // ...
        'routeOptions' => [
            'sudoMode' => [
                'group' => 'systemMaintainer',
                'lifetime' => AccessLifetime::M,
            ],
        ],
    ],
];
Copied!

See also Custom backend modules requiring the sudo mode.

Third-level modules / module functions 

Third-level modules are registered in the extension's Configuration/Backend/Modules.php file, the same way as top-level and common modules.

This allows administrators to define access permissions via the module access logic for those modules individually. It also allows to influence the position of the third-level module.

Example 

Registration of an additional third-level module for the Web > Template module in the Configuration/Backend/Modules.php file of an extension:

EXT:my_extension/Configuration/Backend/Modules.php
'web_ts_customts' => [
    'parent' => 'web_ts',
    'access' => 'user',
    'path' => '/module/web/typoscript/custom-ts',
    'iconIdentifier' => 'module-custom-ts',
    'labels' => [
        'title' => 'LLL:EXT:extkey/Resources/Private/Language/locallang.xlf:mod_title',
    ],
    'routes' => [
        '_default' => [
            'target' => CustomTsController::class . '::handleRequest',
        ],
    ],
    'moduleData' => [
        'someOption' => false,
    ],
],
Copied!

Toplevel modules 

The following toplevel modules are provided by the Core:

web: Web
All modules requiring a page tree by default. These modules are mostly used to manage content that should be displayed in the frontend.
site: Site Management
Settings for the complete site such as redirects and site settings.
file: File
All modules requiring a file system tree such as modules dealing with file metadata, uploading etc.
tools: Admin Tools

By convention modules in this toplevel section should only be available for admins with system maintainer rights. Therefore the configuration array of a module displayed here should always have the following key-value pair: 'access' => 'systemMaintainer'.

In this toplevel section modules that deal with installing and updating the Core and extensions are available. System-wide settings are also found here.

system: System

By convention, modules in this toplevel section should only be accessible by admins. Therefore the configuration array of a module displayed here should always have the following key-value pair: 'access' => 'admin'.

In this toplevel section modules are situated that deal with backend user rights or might reveal security relevant data.

Register a custom toplevel module 

Toplevel modules like Web or File are registered in the Configuration/Backend/Modules.php. All toplevel modules provided by the Core are registered in EXT:core so you can look at typo3/sysext/core/Configuration/Backend/Modules.php for reference.

Example: 

Register a new toplevel module in your extension:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'myextension' => [
        'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod_web.xlf',
        'iconIdentifier' => 'modulegroup-myextension',
        'navigationComponent' => '@typo3/backend/page-tree/page-tree-element',
    ]
];
Copied!

Backend GUI 

The backend user interface is essentially driven by the "backend" system extension and extended by many other system extensions.

It is divided into the following main areas:

An overview of the visual structure of the backend

Top bar

The top bar is always present. It is itself divided into two areas: the logo and top bar tools.

The logo can be changed using the $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['backend']['backendLogo'] option. Additional top bar tools can be registered using $GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'] .

Module menu

This is the main navigation. All modules are structured in main modules (which can be collapsed) and submodules which is where the action really happens.

The module menu can be opened or closed by using the icon on the top left.

The chapter Modules.php - Backend module configuration describes how new main or submodules are registered.

Navigation frame

Any backend module may have a navigation frame or not. This frame will typically display the page tree or the folder tree, but custom navigation frames are possible.

The current location (i.e. page or frame) is carried over between navigation frames when changing modules. This means, for example, that when you move from the Web > Page module to the Web > List module, the same page stays selected in the page tree.

DocHeader
This part is always located above the Content area. It will generally contain a drop-down menu called the "Function menu", which allows to navigate into the various functions offered by the module. When editing it will also contain all the buttons necessary for saving, closing or reverting. It may contain additional buttons for shortcuts or any specific feature needed by the module.
Content area
This is the actual work area. Any information to show or content to edit will be displayed here.
Contextual menus

(Right) clicking on record icons will often reveal a contextual menu. New functions can be added to the contextual menus, but the mechanisms vary: the page tree behaves differently than the rest of the backend.

A typical contextual menu appears when clicking on a record icon

DocHeaderComponent 

The \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent can be used to display a standardized header section in a backend module with buttons, menus etc. It can also be used to hide the header section in case it is not desired to display it.

The module header displayed by the DocHeaderComponent

You can get the DocHeaderComponent with \TYPO3\CMS\Backend\Template\ModuleTemplate::getDocHeaderComponent from your module template.

DocHeaderComponent API 

It has the following methods:

class DocHeaderComponent
Fully qualified name
\TYPO3\CMS\Backend\Template\Components\DocHeaderComponent

DocHeader component class

setMetaInformation ( array $metaInformation)

Set page information

param $metaInformation

Record array

setMetaInformationForResource ( \TYPO3\CMS\Core\Resource\ResourceInterface $resource)
param $resource

the resource

getMenuRegistry ( )

Get moduleMenuRegistry

Returns
\MenuRegistry
getButtonBar ( )

Get ButtonBar

Returns
\ButtonBar
isEnabled ( )

Determines whether this components is enabled.

Returns
bool
enable ( )

Sets the enabled property to TRUE.

disable ( )

Sets the enabled property to FALSE (disabled).

docHeaderContent ( )

Returns the abstract content of the docHeader as an array

Returns
array

Example: Build a module header with buttons and a menu 

The following example is extracted from the example Extbase extension t3docs/blog-example . See the complete source code at t3doc/blog-example (GitHub).

We use the DocHeaderComponent to register buttons and a menu to the module header.

Class T3docs\BlogExample\Controller\BackendController
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;

class BackendController extends ActionController
{
    private function modifyDocHeaderComponent(ModuleTemplate $view, string &$context): void
    {
        $menu = $this->buildMenu($view, $context);
        $view->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);

        $buttonBar = $view->getDocHeaderComponent()->getButtonBar();
        $this->addButtons($buttonBar);

        $metaInformation = $this->getMetaInformation();
        if (is_array($metaInformation)) {
            $view->getDocHeaderComponent()->setMetaInformation($metaInformation);
        }
    }

    protected function initializeModuleTemplate(
        ServerRequestInterface $request,
    ): ModuleTemplate {
        $view = $this->moduleTemplateFactory->create($request);

        $context = '';
        $this->modifyDocHeaderComponent($view, $context);
        $view->setFlashMessageQueue($this->getFlashMessageQueue());
        $view->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:blog_example/Resources/Private/Language/Module/locallang_mod.xlf:mlang_tabs_tab'),
            $context,
        );

        return $view;
    }
}
Copied!

Module data object 

New in version 12.0

The \TYPO3\CMS\Backend\Module\ModuleData object contains the user specific module settings, for example whether the clipboard is shown, for the requested module. Those settings are fetched from the user's session. A PSR-15 middleware automatically creates the object from the stored user data and attaches it to the PSR-7 Request.

The \TYPO3\CMS\Backend\Module\ModuleData object is available as attribute of the PSR-7 Request - in case a TYPO3 backend module is requested - and contains the stored module data, which might have been overwritten through the current request (with GET / POST).

Through the module registration one can define, which properties can be overwritten via GET / POST and their default value.

The whole determination is done before the requested route target - usually a backend controller - is called. This means, the route target can read the final module data.

The allowed properties are defined with their default value in the module registration:

EXT:my_extension/Configuration/Backend/Modules.php
'moduleData' => [
    'allowedProperty' => '',
    'anotherAllowedProperty' => true,
],
Copied!
EXT:my_extension/Classes/Controller/MyController.php
$MOD_SETTINGS = $request->getAttribute('moduleData');
Copied!

The ModuleData object provides the following methods:

Method Parameters Description
createFromModule() $module $data Create a new object for the given module, while overwriting the default values with $data.
getModuleIdentifier()   Returns the related module identifier
get() $propertyName $default Returns the value for $propertyName, or the $default, if not set.
set() $propertyName $value Updates $propertyName with the given $value.
has() $propertyName Whether $propertyName exists.
clean() $propertyName $allowedValues Cleans a single property by the given allowed list and falls back to either the default value or the first allowed value.
cleanUp() $allowedData $useKeys Cleans up all module data, which are defined in the given allowed data list. Usually called with $MOD_MENU in a controller with module menu.
toArray()   Returns the module data as array.

In case a controller needs to store changed module data, this can still be done using $backendUser->pushModuleData('my_module', $this->moduleData->toArray());.

To restrict the values of module data properties, the given ModuleData object can be cleaned, for example, in a controller:

EXT:my_extension/Classes/Controller/MyController.php
$allowedValues = ['foo', 'bar'];
$this->moduleData->clean('property', $allowedValues);
Copied!

If ModuleData contains property, the value is checked against the $allowedValues list. If the current value is valid, nothing happens. Otherwise the value is either changed to the default or if this value is also not allowed, to the first allowed value.

ModuleInterface 

The registered backend modules are stored as objects in a registry and can be fetched using the \TYPO3\CMS\Backend\Module\ModuleProvider. All module objects implement the \TYPO3\CMS\Backend\Module\ModuleInterface .

The ModuleInterface basically provides getters for the options defined in the module registration and additionally provides methods for relation handling (main modules and sub modules).

Table of contents

ModuleInterface API 

interface ModuleInterface
Fully qualified name
\TYPO3\CMS\Backend\Module\ModuleInterface

An interface representing a TYPO3 Backend module.

getIdentifier ( )

The internal name of the module, used for referencing in permissions etc

Returns
string
getPath ( )

Return the main route path

Returns
string
getIconIdentifier ( )

The icon identifier for the module

Returns
string
getTitle ( )

The title of the module, used in the menu

Returns
string
getDescription ( )

A longer description, common for the "About" section with a long explanation

Returns
string
getShortDescription ( )

A shorter description, used when hovering over a module in the menu as title attribute

Returns
string
isStandalone ( )

Useful for main modules that are also "clickable" such as the dashboard module

Returns
bool
getComponent ( )

Returns the view component responsible for rendering the module (iFrame or name of the web component)

Returns
string
getNavigationComponent ( )

The web component to be rendering the navigation area

Returns
string
getPosition ( )

The position of the module, such as [top] or [bottom] or [after => anotherModule] or [before => anotherModule]

Returns
array
getAppearance ( )

Returns a modules appearance options, e.g. used for module menu

Returns
array
getAccess ( )

Can be user (editor permissions), admin, or systemMaintainer

Returns
string
getWorkspaceAccess ( )

Can be "*" (= empty) or "live" or "offline"

Returns
string
getParentIdentifier ( )

The identifier of the parent module during registration

Returns
string
getParentModule ( )

Get the reference to the next upper menu item

Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
hasParentModule ( )

Can be checked if the module is a "main module"

Returns
bool
hasSubModule ( string $identifier)

Checks whether this module has a submodule with the given identifier

param $identifier

the identifier

Returns
bool
hasSubModules ( )

Checks if this module has further submodules

Returns
bool
getSubModule ( string $identifier)

Return a submodule given by its full identifier

param $identifier

the identifier

Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
getSubModules ( )

Return all direct descendants of this module

Returns
\ModuleInterface[]
getDefaultRouteOptions ( )

Returns module related route options - used for the router

Returns
array
getDefaultModuleData ( )

Get allowed and available module data properties and their default values.

Returns
array
getAliases ( )

Return a list of identifiers that are aliases to this module

Returns
array

ModuleProvider 

The ModuleProvider API allows extension authors to work with the registered modules.

This API is the central point to retrieve modules, since it automatically performs necessary access checks and prepares specific structures, for example for the use in menus.

ModuleProvider API 

class ModuleProvider
Fully qualified name
\TYPO3\CMS\Backend\Module\ModuleProvider

This is the central point to retrieve modules from the ModuleRegistry, while performing the necessary access checks, which ModuleRegistry does not deal with.

isModuleRegistered ( string $identifier)

Simple wrapper for the registry, which just checks if a module is registered. Does NOT perform any access checks.

param $identifier

the identifier

Returns
bool
getModule ( string $identifier, ?\TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user = NULL, bool $respectWorkspaceRestrictions = true)

Returns a Module for the given identifier. In case a user is given, also access checks are performed.

param $identifier

the identifier

param $user

the user, default: NULL

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

Returns
?\TYPO3\CMS\Backend\Module\ModuleInterface
getModules ( ?\TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user = NULL, bool $respectWorkspaceRestrictions = true, bool $grouped = true)

Returns all modules either grouped by main modules or flat.

In case a user is given, also access checks are performed.

param $user

the user, default: NULL

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

param $grouped

the grouped, default: true

Returns
\ModuleInterface[]
getModuleForMenu ( string $identifier, \TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user, bool $respectWorkspaceRestrictions = true)

Return the requested (main) module if exist and allowed, prepared for menu generation or similar structured output (nested). Takes TSConfig into account. Does not respect "appearance[renderInModuleMenu]".

param $identifier

the identifier

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

Returns
?\TYPO3\CMS\Backend\Module\MenuModule
getModulesForModuleMenu ( \TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user, bool $respectWorkspaceRestrictions = true)

Returns all allowed modules for the current user, prepared for module menu generation or similar structured output (nested).

Takes TSConfig and "appearance[renderInModuleMenu]" into account.

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

Returns
\MenuModule[]
accessGranted ( string $identifier, \TYPO3\CMS\Core\Authentication\BackendUserAuthentication $user, bool $respectWorkspaceRestrictions = true)

Check access of a module for a given user

param $identifier

the identifier

param $user

the user

param $respectWorkspaceRestrictions

the respectWorkspaceRestrictions, default: true

Returns
bool

ModuleTemplate 

Backend controllers should use ModuleTemplateFactory::create() to create instances of a \TYPO3\CMS\Backend\Template\ModuleTemplate .

API functions of the ModuleTemplate can be used to add buttons to the button bar. It also implements the \TYPO3\CMS\Core\View\ViewInterface so values can be assigned to it in the actions.

class ModuleTemplate
Fully qualified name
\TYPO3\CMS\Backend\Template\ModuleTemplate

A class taking care of the "outer" HTML of a module, especially the doc header and other related parts.

assign ( string $key, ?mixed $value)

Add a variable to the view data collection.

param $key

the key

param $value

the value

Returns
self
assignMultiple ( array $values)

Add multiple variables to the view data collection.

param $values

the values

Returns
self
render ( string $templateFileName = '')

Render the module.

param $templateFileName

the templateFileName, default: ''

Returns
string
renderResponse ( string $templateFileName = '')

Render the module and create an HTML 200 response from it. This is a lazy shortcut so controllers don't need to take care of this in the backend.

param $templateFileName

the templateFileName, default: ''

Returns
\Psr\Http\Message\ResponseInterface
setBodyTag ( string $bodyTag)

Set to something like '<body id="foo">' when a special body tag is needed.

param $bodyTag

the bodyTag

Returns
self
setTitle ( string $title, string $context = '')

Title string of the module: "My module · Edit view"

param $title

the title

param $context

the context, default: ''

Returns
self
getDocHeaderComponent ( )

Get the DocHeader. Can be used in controllers to add custom buttons / menus / ... to the doc header.

Returns
\TYPO3\CMS\Backend\Template\Components\DocHeaderComponent
setForm ( string $formTag = '')

A "<form>" tag encapsulating the entire module, including doc-header.

param $formTag

the formTag, default: ''

Returns
self
setModuleId ( string $moduleId)

Optional 'data-module-id="{moduleId}"' on first <div> in body.

Can be helpful in JavaScript.

param $moduleId

the moduleId

Returns
self
setModuleName ( string $moduleName)

Optional 'data-module-name="{moduleName}"' on first <div> in body.

Can be helpful in JavaScript.

param $moduleName

the moduleName

Returns
self
setModuleClass ( string $moduleClass)

Optional 'class="module {moduleClass}"' on first <div> in body.

Can be helpful styling modules.

param $moduleClass

the moduleClass

Returns
self
addFlashMessage ( string $messageBody, string $messageTitle = '', \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity $severity = \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::OK, bool $storeInSession = true)

Creates a message object and adds it to the FlashMessageQueue.

These messages are automatically rendered when the view is rendered.

param $messageBody

the messageBody

param $messageTitle

the messageTitle, default: ''

param $severity

the severity, default: TYPO3CMSCoreTypeContextualFeedbackSeverity::OK

param $storeInSession

the storeInSession, default: true

Returns
self
setFlashMessageQueue ( \TYPO3\CMS\Core\Messaging\FlashMessageQueue $flashMessageQueue)

ModuleTemplate by default uses queue 'core.template.flashMessages'. Modules may want to maintain an own queue. Use this method to render flash messages of a non-default queue at the default position in module HTML output. Call this method before adding single messages with addFlashMessage().

param $flashMessageQueue

the flashMessageQueue

Returns
self
setUiBlock ( bool $uiBlock)

UI block is a spinner shown during browser rendering phase of the module, automatically removed when rendering finished. This is done by default, but the UI block can be turned off when needed for whatever reason.

param $uiBlock

the uiBlock

Returns
self
makeDocHeaderModuleMenu ( array $additionalQueryParams = [])

Generates a menu in the docheader to access third-level modules

param $additionalQueryParams

the additionalQueryParams, default: []

Returns
self

Example: Create and use a ModuleTemplate in an Extbase Controller 

The following example is extracted from the example Extbase extension t3docs/blog-example . See the complete source code at t3doc/blog-example (GitHub).

Class T3docs\BlogExample\Controller\BackendController
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use T3docs\BlogExample\Domain\Model\Post;
use TYPO3\CMS\Backend\Template\ModuleTemplate;

class BackendController extends ActionController
{
    protected function initializeModuleTemplate(
        ServerRequestInterface $request,
    ): ModuleTemplate {
        $view = $this->moduleTemplateFactory->create($request);

        $context = '';
        $this->modifyDocHeaderComponent($view, $context);
        $view->setFlashMessageQueue($this->getFlashMessageQueue());
        $view->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:blog_example/Resources/Private/Language/Module/locallang_mod.xlf:mlang_tabs_tab'),
            $context,
        );

        return $view;
    }

    public function showPostAction(Post $post): ResponseInterface
    {
        $view = $this->initializeModuleTemplate($this->request);
        $view->assign('post', $post);
        return $view->renderResponse('ShowPost');
    }
}
Copied!

ModuleTemplateFactory 

The template module factory should be used by backend controllers to create a TYPO3CMSBackendTemplateModuleTemplate.

Table of contents

ModuleTemplateFactory API 

class ModuleTemplateFactory
Fully qualified name
\TYPO3\CMS\Backend\Template\ModuleTemplateFactory

A factory class creating backend related ModuleTemplate view objects.

create ( \Psr\Http\Message\ServerRequestInterface $request)
param $request

the request

Returns
\TYPO3\CMS\Backend\Template\ModuleTemplate

Example: Initialize module template 

In many backend modules all actions should have the same module header. So it is useful to initialize the backend module template in a function commonly used by all actions:

EXT:my_extension/Classes/Controller/BackendModuleController.php
<?php

declare(strict_types=1);

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class BackendModuleController extends ActionController
{
    public function __construct(
        private readonly ModuleTemplateFactory $moduleTemplateFactory,
        // ..
    ) {}

    protected function initializeModuleTemplate(ServerRequestInterface $request): ModuleTemplate
    {
        $moduleTemplate = $this->moduleTemplateFactory->create($request);

        // Add common buttons and menues

        return $moduleTemplate;
    }

    public function someAction(): ResponseInterface
    {
        $moduleTemplate = $this->initializeModuleTemplate($this->request);

        $moduleTemplate->assignMultiple([
            'variable1' => 'value 1',
            'variable2' => 'value 2',
        ]);
        return $moduleTemplate->renderResponse('Backend/Some');
    }

}
Copied!

TypoScript configuration of modules 

The backend module of an extension can be configured via TypoScript. The configuration is done in module.tx_<lowercaseextensionname>_<lowercasepluginname> or in module.tx_<lowercaseextensionname>. If the part _<lowercasepluginname> is omitted, then the setting is used for all backend modules of that extension.

Even in the backend the TypoScript setup is used. The settings should be done globally and not changed on a per-page basis. Therefore they are usually set in the file EXT:my_extension/ext_typoscript_setup.typoscript.

See the toplevel object "module" in the TypoScript reference for the available options.

Sudo mode in TYPO3 backend modules 

When accessing modules in the Admin Tools via backend user interface, currently logged in backend users have to confirm their user password again in order to get access to the modules in this section.

As an alternative, it is also possible to use the install tool password. This is done in order to mitigate unintended modifications that might occur as result of for example possible cross-site scripting vulnerabilities in the system.

Authentication in for sudo mode in extensions using the auth service 

Albeit default local authentication mechanisms are working well, there are side effects for 3rd party extensions that make use of these auth service chains as well - such as multi-factor authentication or single sign-on handling.

As an alternative, it is possible to confirm actions using the Install Tool password, instead of confirming with user's password (which might be handled with separate remote services).

Services that extend authentication with custom additional factors (2FA/MFA) are advised to intercept only valid login requests instead of all authUser invocations.

EXT:my_extension/Classes/Authentication/MyAuthenticationService.php
<?php

namespace MyVendor\MyExtension\Authentication;

use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;

class MyAuthenticationService extends AbstractAuthenticationService
{
    public function authUser(array $user)
    {
        // only handle actual login requests
        if (($this->login['status'] ?? '') !== 'login') {
            // skip this service, hand over to next in chain
            return 100;
        }
        // ...
        // usual processing for valid login requests
        // ...
        return 0;
    }
}
Copied!

Custom backend modules requiring the sudo mode 

In general, the configuration for a particular route or module looks like this:

+ 'sudoMode' => [
+     'group' => 'individual-group-name',
+     'lifetime' => AccessLifetime::veryShort,
+ ],
Copied!
  • group (optional): if given, grants access to other objects of the same group without having to verify sudo mode again for a the given lifetime. Example: Admin Tool modules Maintainance and Settings are configured with the same systemMaintainer group - having access to one (after sudo mode verification) grants access to the other automatically.
  • lifetime: enum value of \TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime , defining the lifetime of a sudo mode verification, afterwards users have to go through the process again - cases are veryShort (5 minutes), short (10 minutes), medium (15 minutes), long (30 minutes), veryLong (60 minutes)

For backend routes declared via Configuration/Backend/Routes.php, the relevant configuration would look like this:

EXT:my_extension/Configuration/Backend/Routes.php
<?php

use MyVendor\MyExtension\Handlers\MyHandler;
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime;

return [
    'my-route' => [
        'path' => '/my/route',
        'target' => MyHandler::class . '::process',
        'sudoMode' => [
            'group' => 'mySudoModeGroup',
            'lifetime' => AccessLifetime::S,
        ],
    ],
];
Copied!

For backend modules declared via Configuration/Backend/Modules.php, the relevant configuration would look like this:

EXT:my_extension/Configuration/Backend/Modules.php
<?php

use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime;

return [
    'tools_ExtensionmanagerExtensionmanager' => [
        // ...
        'routeOptions' => [
            'sudoMode' => [
                'group' => 'systemMaintainer',
                'lifetime' => AccessLifetime::M,
            ],
        ],
    ],
];
Copied!

Process in a nutshell 

All simplified classnames below are located in the namespace \TYPO3\CMS\Backend\Security\SudoMode\Access). The low-level request orchestration happens in the middleware \TYPO3\CMS\Backend\Middleware\SudoModeInterceptor , markup rendering and payload processing in controller \TYPO3\CMS\Backend\Controller\Security\SudoModeController .

  1. A backend route is processed, that requires sudo mode for route URI /my/route in \TYPO3\CMS\Backend\Http\RouteDispatcher .
  2. Using AccessFactory and AccessStorage , the \RouteDispatcher tries to find a valid and not expired AccessGrant item for the specific RouteAccessSubject('/my/route') aspect in the current backend user session data.
  3. In case no AccessGrant can be determined, a new AccessClaim is created for the specific RouteAccessSubject instance and temporarily persisted in the current user session data - the claim also contains the originally requested route as ServerRequestInstruction (a simplified representation of a \ServerRequestInterface).
  4. Next, the user is redirected to the user interface for providing either their own password, or the global install tool password as alternative.
  5. Given, the password was correct, the AccessClaim is "converted" to an AccessGrant , which is only valid for the specific subject (URI /my/route) and for a limited lifetime.

JavaScript in TYPO3 backend 

Some third-party JavaScript libraries are packaged with the TYPO3 source code. The TYPO3 backend itself relies on quite a lot of JavaScript to do its job. The topic of this chapter is to present how to use JavaScript properly with TYPO3, in particular in the backend. It presents the most important APIs in that regard.

Contents:

ES6 in the TYPO3 Backend 

Changed in version 12.0

Starting with TYPO3 v12.0 JavaScript ES6 modules may be used instead of AMD modules, both in backend and frontend context.

JavaScript node-js style path resolutions are managed by import maps, which allow web pages to control the behavior of JavaScript imports.

In November 2022 import maps are supported natively by Google Chrome, a polyfill is available for Firefox and Safari and included by TYPO3 Core and applied whenever an import map is emitted.

For security reasons, import map configuration is only emitted when the modules are actually used, that means when a module has been added to the current page response via PageRenderer->loadJavaScriptModule() or JavaScriptRenderer->addJavaScriptModuleInstruction(). Exposing all module configurations is possible via JavaScriptRenderer->includeAllImports(), but that should only be done in backend context for logged-in users to avoid disclosing installed extensions to anonymous visitors.

Configuration 

A simple configuration example for an extension that maps the Public/JavaScript folder to an import prefix @vendor/my-extensions:

EXT:my_extension/Configuration/JavaScriptModules.php
<?php

return [
    // required import configurations of other extensions,
    // in case a module imports from another package
    'dependencies' => ['backend'],
    'imports' => [
        // recursive definiton, all *.js files in this folder are import-mapped
        // trailing slash is required per importmap-specification
        '@vendor/my-extension/' => 'EXT:my_extension/Resources/Public/JavaScript/',
    ],
];
Copied!

Complex configuration example containing recursive-lookup exclusions, third-party library definitions and overwrites:

EXT:my_extension/Configuration/JavaScriptModules.php
<?php

return [
    'dependencies' => ['core', 'backend'],
    'imports' => [
        '@vendor/my-extension/' => [
            'path' => 'EXT:my_extension/Resources/Public/JavaScript/',
            // Exclude files of the following folders from being import-mapped
            'exclude' => [
                'EXT:my_extension/Resources/Public/JavaScript/Contrib/',
                'EXT:my_extension/Resources/Public/JavaScript/Overrides/',
            ],
        ],
        // Adding a third party package
        'thirdpartypkg' => 'EXT:my_extension/Resources/Public/JavaScript/Contrib/thidpartypkg/index.js',
        'thidpartypkg/' => 'EXT:my_extension/Resources/Public/JavaScript/Contrib/thirdpartypkg/',
        // Overriding a file from another package
        '@typo3/backend/modal.js' => 'EXT:my_extension/Resources/Public/JavaScript/Overrides/BackendModal.js',
    ],
];
Copied!

Loading ES6 

A module can be added to the current page response either via PageRenderer or as JavaScriptModuleInstruction via JavaScriptRenderer:

EXT:my_extension/Classes/SomeNamespace/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\SomeNamespace;

use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Page\PageRenderer;

final class SomeClass
{
    public function __construct(
        // Inject the page renderer dependency
        private readonly PageRenderer $pageRenderer,
    ) {}

    public function someFunction()
    {
        // Load JavaScript via PageRenderer
        $this->pageRenderer->loadJavaScriptModule('@vendor/my-extension/example.js');

        // Load JavaScript via JavaScriptRenderer
        $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
            JavaScriptModuleInstruction::create('@vendor/my-extension/example.js'),
        );
    }
}
Copied!

In a Fluid template the includeJavaScriptModules property of the <f:be.pageRenderer> ViewHelper may be used:

EXT:my_extension/Resources/Private/Backend/Templates/SomeTemplate.html

<f:be.pageRenderer
    includeJavaScriptModules="{
         0: '@myvendor/my-extension/example.js'
      }"
/>
Copied!

Some tips on ES6 

No ES6 JavaScript files are created directly in the TYPO3 Core. JavaScript is created as TypeScript module which is then converted to ES6 JavaScript during the build process. However, TypeScript and ES6 are quite similar, you can therefore look into those files for reference. The TypeScript files can be found on GitHub at Build/Sources/TypeScript.

For examples of an ES6 JavaScript file have a look at the JavaScript example in the LinkHandler Tutorial or the example in the Notification API.

For a practical example on how to introduce ES6 modules into a large extension see this commit for EXT:news: [TASK] Add support for TYPO3 v12 ES6 modules.

Using JQuery 

In the TYPO3 Core usage of jQuery is eliminated step-by-step as the necessary functionality is provided by native JavaScript nowadays.

If you still have to use jQuery in your third-party extension, include it with the following statement:

import $ from 'jquery';
Copied!

Add JavaScript modules to import map in backend form 

The JavaScript module import map is static and only generated and loaded in the first request to a document. All possible future modules requested in later Ajax calls need to be registered already in the first initial request.

The tag backend.form is used to identify JavaScript modules that can be used within backend forms. This ensures that the import maps are available for these modules even if the element is not displayed directly.

A typical use case for this is an InlineRelationRecord where the CKEditor is not part of the main record but needs to be loaded for the child record.

EXT:my_extension/Configuration/JavaScriptModules.php
<?php

return [
    'dependencies' => [
        'backend',
    ],
    'tags' => [
        'backend.form',
    ],
    'imports' => [
        '@typo3/rte-ckeditor/' => 'EXT:rte_ckeditor/Resources/Public/JavaScript/',
        '@typo3/ckeditor5-bundle.js' => 'EXT:rte_ckeditor/Resources/Public/Contrib/ckeditor5-bundle.js',
    ],
];
Copied!

Client-side templating 

To avoid custom jQuery template building a slim client-side templating engine lit-html together with lit-element is used in the TYPO3 Core.

This templating engine supports conditions, iterations, events, virtual DOM, data-binding and mutation/change detections in templates.

Individual client-side templates can be processed in JavaScript directly using modern web technologies like template-strings and template-elements.

Rendering is handled by the AMD-modules lit-html and lit-element. Please consult the lit-html template-reference and the lit-element-guide for more information.

Examples 

Variable assignment 

import {html, render} from 'lit-html';

const value = 'World';
const target = document.getElementById('target');
render(html`<div>Hello ${value}!</div>`, target);
Copied!
<div>Hello World!</div>
Copied!

Unsafe tags would have been encoded (e.g. <b>World</b> as &lt;b&gt;World&lt;/b&gt;).

Conditions and iteration 

import {html, render} from 'lit-html';
import {classMap} from 'lit-html/directives/class-map.js';

const items = ['a', 'b', 'c']
const classes = { list: true };
const target = document.getElementById('target');
const template = html`
   <ul class=${classMap(classes)}">
   ${items.map((item: string, index: number): string => {
      return html`<li>#${index+1}: ${item}</li>`
   })}
   </ul>
`;
render(template, target);
Copied!
<ul class="list">
   <li>#1: a</li>
   <li>#2: b</li>
   <li>#3: c</li>
</ul>
Copied!

The ${...} literal used in template tags can basically contain any JavaScript instruction - as long as their result can be casted to string again or is of type lit-html.TemplateResult. This allows to make use of custom conditions as well as iterations:

  • condition: ${condition ? thenReturn : elseReturn}
  • iteration: ${array.map((item) => { return item; })}

Events 

Events can be bound using the @ attribute prefix.

import {html, render} from 'lit-html';

const value = 'World';
const target = document.getElementById('target');
const template = html`
   <div @click="${(evt: Event): void => { console.log(value); }}">
      Hello ${value}!
   </div>
`;
render(template, target);
Copied!

The result won't look much different than the first example - however the custom attribute @click will be transformed into an according event listener bound to the element where it has been declared.

Custom HTML elements 

A web component based on the W3C custom elements ("web-components_") specification can be implemented using lit-element.

import {LitElement, html, customElement, property} from 'lit-element';

@customElement('my-element')
class MyElement extends LitElement {

 // Declare observed properties
 @property()
 value: string = 'awesome';

 // Avoid Shadow DOM so global styles apply to the element contents
 createRenderRoot(): Element|ShadowRoot {
   return this;
 }

 // Define the element's template
 render() {
   return html`<p>Hello ${this.value}!</p>`;
 }
}
Copied!
<my-element value="World"></my-element>
Copied!

This is rendered as:

<my-element value="World">
   <p>Hello world!</p>
</my-element>
Copied!

Modals 

Changed in version 12.0

The modal API provided by the module @typo3/backend/modal.js has been adapted to be backed by a custom web component and therefore gained an updated, stateless interface. See also section Migration.

Actions that require a user's attention must be visualized by modal windows.

TYPO3 provides an API as basis to create modal windows with severity representation. For better UX, if actions (buttons) are attached to the modal, one button must be a positive action. This button should get a btnClass to highlight it.

Modals should be used rarely and only for confirmations. For information that does not require a confirmation the Notification API (flash message) should be used.

For complex content, like forms or a lot of information, use normal pages.

API 

Changed in version 12.0

The return type of all Modal.* factory methods has been changed from JQuery to ModalElement.

The API provides only two public methods:

  1. TYPO3.Modal.confirm(title, content, severity, buttons)
  2. TYPO3.Modal.dismiss()

Button settings 

Name Type
string
function
bool
string

text

text
Type
string
Required
true

The text rendered into the button.

trigger / action

trigger / action
Type
function
Required
true

Callback that is triggered on button click - either a simple function or DeferredAction / ImmediateAction

active

active
Type
bool

Marks the button as active. If true, the button gets the focus.

btnClass

btnClass
Type
string

The CSS class for the button.

Changed in version 12.0

The Button property dataAttributes has been removed without replacement, as the functionality can be expressed via Button.name or Button.trigger and is therefore redundant.

Data Attributes 

It is also possible to use data attributes to trigger a modal, for example on an anchor element, which prevents the default behavior.

data-title
The title text for the modal.
data-content
The content text for the modal.
data-severity
The severity for the modal, default is info (see TYPO3.Severity.*).
data-href
The target URL, default is the href attribute of the element.
data-button-close-text
Button text for the close/cancel button.
data-button-ok-text
Button text for the ok button.
class="t3js-modal-trigger"
Marks the element as modal trigger.
data-static-backdrop
Render a static backdrop to avoid closing the modal when clicking it.

Example:

<a
    href="delete.php"
    class="t3js-modal-trigger"
    data-title="Delete"
    data-content="Really delete?"
>
    delete
</a>
Copied!

Examples 

A basic modal (without anything special) can be created this way:

TYPO3.Modal.confirm('The title of the modal', 'This the the body of the modal');
Copied!

A modal as warning with button:

TYPO3.Modal.confirm('Warning', 'You may break the internet!', TYPO3.Severity.warning, [
  {
    text: 'Break it',
    active: true,
    trigger: function() {
      // break the net
    }
  }, {
    text: 'Abort!',
    trigger: function() {
      TYPO3.Modal.dismiss();
    }
  }
]);
Copied!

A modal as warning:

TYPO3.Modal.confirm('Warning', 'You may break the internet!', TYPO3.Severity.warning);
Copied!

Action buttons in modals created by the TYPO3/CMS/Backend/Modal module may make use of TYPO3/CMS/Backend/ActionButton/ImmediateAction and TYPO3/CMS/Backend/ActionButton/DeferredAction.

As an alternative to the existing trigger option, the option action may be used with an instance of the previously mentioned modules.

Modal.confirm('Header', 'Some content', Severity.error, [
  {
    text: 'Based on trigger()',
    trigger: function () {
      console.log('Vintage!');
    }
  },
  {
    text: 'Based on action',
    action: new DeferredAction(() => {
      return new AjaxRequest('/any/endpoint').post({});
    })
  }
]);
Copied!

Activating any action disables all buttons in the modal. Once the action is done, the modal disappears automatically.

Buttons of the type DeferredAction render a spinner on activation into the button.

A modal with static backdrop:

import Modal from '@typo3/backend/modal.js';

Modal.advanced({
  title: 'Hello',
  content: 'This modal is not closable via clicking the backdrop.',
  size: Modal.sizes.small,
  staticBackdrop: true
});
Copied!

Templates, using the HTML class .t3js-modal-trigger to initialize a modal dialog are also able to use the new option by adding the data-static-backdrop attribute to the corresponding element.

<button class="btn btn-default t3js-modal-trigger"
        data-title="Hello"
        data-content="This modal is not closable via clicking the backdrop."
        data-static-backdrop>
    Open modal
</button>
Copied!

Migration 

Given the following fully-fledged example of a modal that uses custom buttons, with custom attributes, triggers and events, they should be migrated away from JQuery to ModalElement usage.

Existing code:

var configuration = {
   buttons: [
      {
         text: 'Save changes',
         name: 'save',
         icon: 'actions-document-save',
         active: true,
         btnClass: 'btn-primary',
         dataAttributes: {
            action: 'save'
         },
         trigger: function() {
            Modal.currentModal.trigger('modal-dismiss');
         }
      }
   ]
};
Modal
  .advanced(configuration)
  .on('hidden.bs.modal', function() {
    // do something
});
Copied!

Should be adapted to:

const modal = Modal.advanced({
   buttons: [
      {
         text: 'Save changes',
         name: 'save',
         icon: 'actions-document-save',
         active: true,
         btnClass: 'btn-primary',
         trigger: function(event, modal) {
           modal.hideModal();
         }
      }
   ]
});
modal.addEventListener('typo3-modal-hidden', function() {
  // do something
});
Copied!

Multi-step wizard 

The JavaScript module MultiStepWizard can be used to show a modal multi-step wizard with the following features:

  • Navigation to previous / next steps
  • Steps may have descriptive labels like "Start" or "Finish!"
  • Steps may require actions before becoming available.

Add Slide 

You have to define at least one slide MultiStepWizard.addSlide().

Name Type
string
string
string|JQuery|Element|DocumentFragment
SeverityEnum
string
SlideCallback

identifier

identifier
Type
string
Required
true

A unique identifier for the slide

title

title
Type
string
Required
true

The title of the slide. Will be shown as header of the slide.

content

content
Type
string|JQuery|Element|DocumentFragment
Required
true

The content of the slide. If string any HTML will be escaped. To prevent that, chose one of the other allowed types:

JQuery:

$(`<div>Your HTML content</div>`);
Copied!

Element:

Object.assign(document.createElement('div'), {
  innerHTML: 'Your HTML content'
});
Copied!

DocumentFragment:

document.createRange().createContextualFragment("<div>Your HTML content</div>");
Copied!

severity

severity
Type
SeverityEnum
Required
true

Set severity color for sheet. Color will only affect title bar and prev- and next-buttons.

progressBarTitle

progressBarTitle
Type
string
Required
true

Set a title for the progress bar. The progress bar will only be shown below the content section of the slide, if you have defined at least two or more slides.

callback

callback
Type
SlideCallback
Required
true

A JavaScript callback function which will be called after the slide was rendered completely.

Show / Hide Wizard 

After defining some slides you can show MultiStepWizard.show() and hide MultiStepWizard.dismiss() the multi-step wizard.

Lock/Unlock steps 

Switching to the next or previous slides is called a step. The buttons to navigate to the slides are deactivated by default. Please use following methods to lock or unlock them:

MultiStepWizard.lockNextStep();
MultiStepWizard.unlockNextStep();
MultiStepWizard.lockPrevStep();
MultiStepWizard.unlockPrevStep();
Copied!

"Hello world" Example 

This JavaScript snippet will create a new multi-step wizard with just one sheet. As it used SeverityEnum.warning the title and buttons will be colored in yellow.

EXT:my_extension/Resources/Public/JavaScript/HelloWorldModule.js
import {SeverityEnum} from "@typo3/backend/enum/severity.js"
import MultiStepWizard from "@typo3/backend/multi-step-wizard.js"
import $ from "jquery";

export default class HelloWorldModule {
  constructor(triggerHelloWorldWizardButtonClass) {
    const buttons = document.querySelectorAll("." + triggerHelloWorldWizardButtonClass);

    buttons.forEach((button) => {
      button.addEventListener("click", () => {
        MultiStepWizard.addSlide(
          "UniqueHelloWorldIdentifier",
          "Title of the Hello World example slide",
          document.createRange().createContextualFragment("<div>Hello world</div>"),
          SeverityEnum.warning,
          "Step Hello World",
          function ($slide) {
            let $modal = $slide.closest(".modal");
            let $nextButton = $modal.find(".modal-footer").find("button[name='next']");
            MultiStepWizard.unlockNextStep();

            $nextButton.off().on("click", function () {
              // Process whatever you want from current slide, just before wizard will be closed or next slide

              // Close wizard
              MultiStepWizard.dismiss();

              // Go to next slide, if any
              // MultiStepWizard.setup.$carousel.carousel("next");
            });
          }
        );

        MultiStepWizard.show();
      });
    });
  }
}
Copied!

To call the JavaScript from above you have to use the JavaScriptModuleInstruction) technique. In following snippet you see how to add a JavaScript module to field within Form Engine:

$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
    '@stefanfroemken/dropbox/AccessTokenModule.js'
)->instance($fieldId);
Copied!

DocumentService (jQuery.ready substitute) 

The module TYPO3/CMS/Core/DocumentService provides native JavaScript functions to detect DOM ready-state returning a Promise<Document>.

Internally the Promise is resolved when native DOMContentLoaded event has been emitted or when document.readyState is defined already. It means the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and sub-frames to finish loading.

$(document).ready(() => {
  // your application code
});
Copied!

Above jQuery code can be transformed into the following using DocumentService:

import DocumentService from '@typo3/core/document-service.js';

DocumentService.ready().then(() => {
  // your application code
});
Copied!

SessionStorage wrapper 

TYPO3 ships a module acting as a wrapper for sessionStorage. It behaves similar to the localStorage, except that the stored data is dropped after the browser session has ended.

The module TYPO3/CMS/Core/Storage/BrowserSession allows to store data in the sessionStorage.

Example 

import Client from '@typo3/backend/storage/client.js';

Client.set('common-prefix-a', 'a');
Client.set('common-prefix-b', 'b');
Client.set('common-prefix-c', 'c');

const entries = Client.getByPrefix('common-prefix-');
// {'common-prefix-a': 'a', 'common-prefix-b': 'b', 'common-prefix-c': 'c'}
Copied!

API methods 

The module is called @typo3/backend/storage/abstract-client-storage, implemented by:

  • @typo3/backend/storage/browser-session
  • @typo3/backend/storage/client
get(key)
Fetches the data behind the key.
getByPrefix('common-prefix-')
Obtains multiple items prefixed by a given key.
set(key, value)
Sets/overrides a key with any arbitrary content.
isset(key) (bool)
Checks if the key is in use.
unset(key)
Removes a key from the storage.
clear()
Empties all data inside the storage.
unsetByPrefix(prefix)
Empties all data inside the storage with their keys starting with a prefix.

Ajax request 

TYPO3 Core ships an API to send Ajax requests to the server. This API is based on the fetch API, which is implemented in every modern browser (for example, Chrome, Edge, Firefox, Safari).

Prepare a request 

New in version 13.0

Native URL-related objects ( URL and URLSearchParams) can be used.

To be able to send a request, the module @typo3/core/ajax/ajax-request.js must be imported. To prepare a request, create a new instance of AjaxRequest per request and pass the URL as the constructor argument:

EXT:my_extension/Resources/Private/JavaScript/MyRequest.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

let url = 'https://example.org/my-endpoint';
// or:
let url = new URL('https://example.org/my-endpoint');

let request = new AjaxRequest(url);
Copied!

The API offers a method withQueryArguments() which allows to attach a query string to the URL. This comes in handy, if the query string is programmatically generated. The method returns a clone of the AjaxRequest object. It is possible to pass either strings, arrays or objects as an argument.

Example:

EXT:my_extension/Resources/Private/JavaScript/MyRequest.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

let request = new AjaxRequest('https://example.org/my-endpoint');

let queryArguments = {
  foo: 'bar',
  bar: {
    baz: ['foo', 'bencer']
  }
};
// or:
let queryArguments = new URLSearchParams({
  foo: 'bar',
  baz: {
    baz: ['foo', 'bencer']
  }
});

request = request.withQueryArguments(queryArguments);

// The query string compiles to ?foo=bar&bar[baz][0]=foo&bar[baz][1]=bencer
Copied!

The method detects whether the URL already contains a query string and appends the new query string in a proper format.

Send a request 

The API offers some methods to actually send the request:

  • get()
  • post()
  • put()
  • delete()

Each of these methods set the corresponding request method (GET, POST, PUT, DELETE). post(), put() and delete() accept the following arguments:

data
Required

true

type

string | object

The payload to be sent as body in the request.

init
Required

false

type

object

Default

{}

Additional request configuration to be set.

The method get() accepts the init argument only.

Example:

let promise = request.get();
Copied!

The body of the request is automatically converted to a FormData object, if the submitted payload is an object. To send a JSON-encoded object instead, set the Content-Type header to application/json. If the payload is a string, no conversion will happen, but it is still recommended to set proper headers.

Example:

EXT:my_extension/Resources/Private/JavaScript/MyRequest.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

let request = new AjaxRequest('https://example.org/my-endpoint');

const json = {foo: 'bar'};
let promise = request.post(json, {
  headers: {
    'Content-Type': 'application/json; charset=utf-8'
  }
});
Copied!

Handle the response 

In the examples above promise is, as the name already spoils, a Promise object. To fetch the actual response, we make use of then():

EXT:my_extension/Resources/Private/JavaScript/MyRequest.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

let request = new AjaxRequest('https://example.org/my-endpoint');

const json = {foo: 'bar'};
let promise = request.post(json, {
  headers: {
    'Content-Type': 'application/json; charset=utf-8'
  }
});

promise.then(async function (response) {
  const responseText = await response.resolve();
  console.log(responseText);
});
Copied!

response is an object of type AjaxResponse shipped by TYPO3 ( @typo3/core/ajax/ajax-response.js). The object is a simple wrapper for the original Response object. AjaxResponse exposes the following methods which eases the handling of responses:

resolve()
Returns the correct response based on the received Content-Type header, either plaintext or a JSON object.
raw()
Returns the original Response object.

Of course, a request may fail for various reasons. In such case, a second function may be passed to then(), which handles the exceptional case. The function may receive a AjaxResponse object which contains the original response object.

EXT:my_extension/Resources/Private/JavaScript/MyRequest.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

let request = new AjaxRequest('https://example.org/my-endpoint');

const json = {foo: 'bar'};
let promise = request.post(json, {
  headers: {
    'Content-Type': 'application/json; charset=utf-8'
  }
});

promise.then(async function (response) {
}, function (error) {
  console.error(`The request failed with ${error.response.status}: ${error.response.statusText}`);
});
Copied!

Abort a request 

In some cases it might be necessary to abort a running request. The Ajax API has you covered them, an instance of AbortController is attached to each request. To abort the request, just call the abort() method:

request.abort();
Copied!

Event API 

The TYPO3 JavaScript Event API enables JavaScript developers to have a stable event listening interface. The API takes care of common pitfalls like event delegation and clean event unbinding.

Event Binding 

Each event strategy (see below) has two ways to bind a listener to an event:

Direct Binding 

The event listener is bound to the element that triggers the event. This is done by using the method bindTo(), which accepts any element, document and window.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  // Do something
}).bindTo(document.querySelector('#my-element'));
Copied!

Event Delegation 

The event listener is called if the event was triggered to any matching element inside its bound element.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  // Do something
}).delegateTo(document, 'a[data-action="toggle"]');
Copied!

The event listener is now called every time the element matching the selector a[data-action="toggle"] within document is clicked.

Release an event 

Since each event is an object instance, it is sufficient to call release() to detach the event listener.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

const clickEvent = new RegularEvent('click', function (e) {
  // Do something
}).delegateTo(document, 'a[data-action="toggle"]');

// Do more stuff

clickEvent.release();
Copied!

Event strategies 

The Event API brings several strategies to handle event listeners:

RegularEvent 

The RegularEvent attaches a simple event listener to an event and element and has no further tweaks. This is the common use case for event handling.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RegularEvent from '@typo3/core/event/regular-event.js';

new RegularEvent('click', function (e) {
  e.preventDefault();
  window.location.reload();
}).bindTo(document.querySelector('#my-element'));
Copied!

DebounceEvent 

The DebounceEvent is most suitable if an event is triggered quite often but executing the event listener is called only after a certain wait time.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener
  • wait (number) - the amount of milliseconds to wait before the event listener is called

Changed in version 13.0

The parameter immediate has been removed. There is no direct migration possible. An extension author may re-implement the removed behavior manually, or use the ThrottleEvent module, providing a similar behavior.

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import DebounceEvent from '@typo3/core/event/debounce-event.js';

new DebounceEvent('mousewheel', function (e) {
  console.log('Triggered once after 250ms!');
}, 250).bindTo(document);
Copied!

ThrottleEvent 

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener
  • limit (number) - the amount of milliseconds to wait before the event listener is called

The ThrottleEvent is similar to the DebounceEvent. The important difference is that the event listener is called after the configured wait time during the overall event time.

If an event time is about 2000ms and the wait time is configured to be 100ms, the event listener gets called up to 20 times in total (2000 / 100).

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import ThrottleEvent from '@typo3/core/event/throttle-event.js';

new ThrottleEvent('mousewheel', function (e) {
  console.log('Triggered every 100ms!');
}, 100).bindTo(document);
Copied!

RequestAnimationFrameEvent 

The RequestAnimationFrameEvent binds its execution to the browser's RequestAnimationFrame API. It is suitable for event listeners that manipulate the DOM.

Arguments:

  • eventName (string) - the event to listen on
  • callback (function) - the event listener

Example:

EXT:my_extension/Resources/Public/JavaScript/MyScript.js
import RequestAnimationFrameEvent from '@typo3/core/event/request-animation-frame-event.js';

new RequestAnimationFrameEvent('mousewheel', function (e) {
  console.log('Triggered every 16ms (= 60 FPS)!');
});
Copied!

Hotkey API 

New in version 13.0

TYPO3 provides the @typo3/backend/hotkeys.js module that allows developers to register custom keyboard shortcuts in the TYPO3 backend.

It is also possible and highly recommended to register hotkeys in a dedicated scope to avoid conflicts with other hotkeys, perhaps registered by other extensions.

The module provides an enum with common modifier keys: Ctrl, Meta, Alt, and Shift), and also a public property describing the common hotkey modifier based on the user's operating system: Cmd (Meta) on macOS, Ctrl on anything else (this can be normalized via Hotkeys.normalizedCtrlModifierKey. Using any modifier is optional, but highly recommended.

A hotkey is registered with the register() method. The method takes three arguments:

hotkey
An array defining the keys that must be pressed.
handler
A callback that is executed when the hotkey is invoked.
options

An object that configures a hotkey's behavior:

scope

The scope a hotkey is registered in.

allowOnEditables
If false (default), handlers are not executed when an editable element is focussed.
allowRepeat
If false (default), handlers are not executed when the hotkey is pressed for a long time.
bindElement
If given, an aria-keyshortcuts attribute is added to the element. This is recommended for accessibility reasons.

Example 

EXT:my_extension/Resources/Public/JavaScript/hotkey.js
import Hotkeys, {ModifierKeys} from '@typo3/backend/hotkeys.js';

Hotkeys.register([Hotkeys.normalizedCtrlModifierKey, ModifierKeys.SHIFT, 'e'], keyboardEvent => {
  console.log('Triggered on Ctrl/Cmd+Shift+E');
}, {
  scope: 'my-extension/module',
  bindElement: document.querySelector('.some-element')
});

// Get the currently active scope
const currentScope = Hotkeys.getScope();

// Make use of registered scope
Hotkeys.setScope('my-extension/module');
Copied!

JavaScript form helpers 

Empty checkbox handling 

<input
    type="checkbox"
    name="setting"
    value="1"
    data-empty-value="0"
    data-global-event="change"
    data-action-navigate="$data=~s/$value/"
>
Copied!

Checkboxes used to send a particular value when unchecked can be achieved by using data-empty-value="0". If this attribute is omitted, an empty string '' is sent.

Submitting a form on change 

<input type="checkbox" data-global-event="change" data-action-submit="$form">
<!-- ... or (using CSS selector) ... -->
<input type="checkbox" data-global-event="change" data-action-submit="#formIdentifier">
Copied!

Submits a form once a value has been changed. ($form refers to the parent form element, using CSS selectors like #formIdentifier is possible as well)

Ajax in the backend 

An Ajax endpoint in the TYPO3 backend is usually implemented as a method in a regular controller. The method receives a request object implementing the \Psr\Http\Message\ServerRequestInterface , which allows to access all aspects of the requests and returns an appropriate response in a normalized way. This approach is standardized as PSR-7.

Create a controller 

By convention, a controller is placed within the extension's Controller/ directory, optionally in a subdirectory. To have such controller, create a new ExampleController in Classes/Controller/ExampleController.php inside your extension.

The controller needs not that much logic right now. We create a method called doSomethingAction() which will be our Ajax endpoint.

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        // TODO: return ResponseInterface
    }
}
Copied!

In its current state, the method does nothing yet. We can add a very generic handling that exponentiates an incoming number by 2. The incoming value will be passed as a query string argument named input.

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        // TODO: return ResponseInterface
    }
}
Copied!

We have computed our result by using the exponentiation operator, but we do nothing with it yet. It is time to build a proper response:

EXT:my_extension/Classes/Controller/ExampleController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class ExampleController
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
    ) {}

    public function doSomethingAction(ServerRequestInterface $request): ResponseInterface
    {
        $input = $request->getQueryParams()['input']
            ?? throw new \InvalidArgumentException(
                'Please provide a number',
                1580585107,
            );

        $result = $input ** 2;

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json; charset=utf-8');
        $response->getBody()->write(
            json_encode(['result' => $result], JSON_THROW_ON_ERROR),
        );
        return $response;
    }
}
Copied!

Register the endpoint 

The endpoint must be registered as route. Create a file called Configuration/Backend/AjaxRoutes.php in your extension. The file basically just returns an array of route definitions. Every route in this file will be exposed to JavaScript automatically. Let us register our endpoint now:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php

use MyVendor\MyExtension\Controller\ExampleController;

return [
    'myextension_example_dosomething' => [
        'path' => '/my-extension/example/do-something',
        'target' => ExampleController::class . '::doSomethingAction',
    ],
];
Copied!

The naming of the key myextension_example_dosomething and path /my-extension/example/do-something are up to you, but should contain the extension name, controller name and action name to avoid potential conflicts with other existing routes.

Protect the endpoint 

Make sure to protect your endpoint against unauthorized access, if it performs actions which are limited to authorized backend users only.

Inherit access from backend module 

New in version 12.4.37 / 13.4.18

This functionality was introduced in response to security advisory TYPO3-CORE-SA-2025-021 to mitigate broken access control in backend AJAX routes.

If your endpoint is part of a backend module, you can configure your endpoint to inherit access rights from this specific module by using the configuration option inheritAccessFromModule:

EXT:my_extension/Configuration/Backend/AjaxRoutes.php
<?php

use MyVendor\MyExtension\Controller\ExampleController;

return [
    'myextension_example_dosomething' => [
        'path' => '/my-extension/example/do-something',
        'target' => ExampleController::class . '::doSomethingAction',
        'inheritAccessFromModule' => 'my_module',
    ],
];
Copied!

Use permission checks on standalone endpoints 

In case you're providing a standalone endpoint (that is, the endpoint is not bound to a specific backend module), make sure to perform proper permission checks on your own. You can use the backend user object to perform various authorization and permission checks on incoming requests.

Use in Ajax 

Since the route is registered in AjaxRoutes.php it is exposed to JavaScript now and stored in the global TYPO3.settings.ajaxUrls object identified by the used key in the registration. In this example it is TYPO3.settings.ajaxUrls.myextension_example_dosomething.

Now you are free to use the endpoint in any of your Ajax calls. To complete this example, we will ask the server to compute our input and write the result into the console.

EXT:my_extension/Resources/Public/JavaScript/Calculate.js
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";

// Generate a random number between 1 and 32
const randomNumber = Math.ceil(Math.random() * 32);
new AjaxRequest(TYPO3.settings.ajaxUrls.myextension_example_dosomething)
  .withQueryArguments({input: randomNumber})
  .get()
  .then(async function (response) {
    const resolved = await response.resolve();
    console.log(resolved.result);
  });
Copied!

Backend layout 

Backend layouts can be defined as database records or via page TSconfig. Page TSconfig should be preferred as it can be stored in the file system and be kept under version control.

Backend layout video 

Benjamin Kott: How to implement frontend layouts in TYPO3 using backend layouts

Backend layout configuration 

The backend layout to be used can be configurated for each page and/or a pages' subpages in the Page properties > Appearance. Multiple backend layouts are available if an extension providing backend layouts is installed or backend layouts have been defined as records or page TSconfig.

Choose the backend layout in the page properties

The Info module gives an overview of the backend layouts configured or inherited from a parent page at Web > Info > Pagetree overview > Type: Layouts:

Overview of the backend layouts used

Backend layout definition 

Backend layouts can be configured either as "backend layout" record in a sysfolder or as page TSconfig entry in mod.web_layout.BackendLayouts. Each layout will be saved with a key. The "backend layout" records are using their uid as a key, therefore layouts defined via page TSconfig should use a non-numeric string key. It is a good practice to use a descriptive name as key.

The entries title and icon are being used to display the backend layout options in the page properties.

The overall grid size will be defined by config.backend_layout.colCount and rowCount. Additional rows in the rows array and additional columns in the each rows columns section will be ignored when they are greater than rowCount or colCount respectively.

Each column position can span several columns and or several rows. Each column position must have a distinct number between 0 and n. It is best practice to always assign "0" to the main column if there is such a thing as a main column. Multiple backend layouts that contain similar parts, i.e. header, footer, aside, ... should each have assigned the same number within one project. This leads to a uniform position of the content, which makes it more clear for further use.

For usage with the page-content data processor, an identifier string must be assigned to each column. The default backend layout definition uses identifier = main for column 0.

Backend layout simple example 

The following page TSconfig example creates a simple backend layout consisting of two rows and just one column.

mod {
  web_layout {
    BackendLayouts {
      exampleKey {
        title = Example
        config {
          backend_layout {
            colCount = 1
            rowCount = 2
            rows {
              1 {
                columns {
                  1 {
                    identifier = border
                    name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.3
                    colPos = 3
                    colspan = 1
                  }
                }
              }
              2 {
                columns {
                  1 {
                    identifier = main
                    name = Main
                    colPos = 0
                    colspan = 1
                  }
                }
              }
            }
          }
        }
        icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif
      }
    }
  }
}
Copied!

Backend layout advanced example 

The following page TSconfig example creates a 3x3 backend layout with 5 column position sections in total. The topmost row (here called "header") spans all 3 columns. There is an "aside" spanning two rows on the right.

mod.web_layout.BackendLayouts {
  exampleKey {
    title = Example
    icon = EXT:example_extension/Resources/Public/Images/BackendLayouts/default.gif
    config {
      backend_layout {
        colCount = 3
        rowCount = 3
        rows {
          1 {
            columns {
              1 {
                identifier = header
                name = Header
                colspan = 3
                colPos = 1
              }
            }
          }
          2 {
            columns {
              1 {
                identifier = main
                name = Main
                colspan = 2
                colPos = 0
              }
              2 {
                identifier = aside
                name = Aside
                rowspan = 2
                colPos = 2
              }
            }
          }
          3 {
            columns {
              1 {
                identifier = left
                name = Main Left
                colPos = 5
              }
              2 {
                identifier = right
                name = Main Right
                colPos = 6
              }
            }
          }
        }
      }
    }
  }
}
Copied!

Output of a backend layout in the frontend 

The backend layout to be used on a certain page gets determined either by the backend layout being chosen directly and stored in the pages field "backend_layout" or by the field "backend_layout_next_level" of a parent page up the rootline.

To avoid complex TypoScript for integrators, the handling of backend layouts has been simplified for the frontend.

To get the correct backend layout, the following TypoScript code can be used:

page.10 = FLUIDTEMPLATE
page.10 {
  file.stdWrap.cObject = CASE
  file.stdWrap.cObject {
	key.data = pagelayout

	default = TEXT
	default.value = EXT:sitepackage/Resources/Private/Templates/Home.html

	3 = TEXT
	3.value = EXT:sitepackage/Resources/Private/Templates/1-col.html

	4 = TEXT
	4.value = EXT:sitepackage/Resources/Private/Templates/2-col.html
  }
}
Copied!

Using data = pagelayout is the same as using as

field = backend_layout
ifEmpty.data = levelfield:-2,backend_layout_next_level,slide
ifEmpty.ifEmpty = default
Copied!

In the Fluid template the column positions can be accessed now via content mapping as described here Display the content elements on your page.

Reference implementations of backend layouts 

The extension bk2k/bootstrap-package ships several Backend layouts as well as an example configuration of how to include frontend templates for backend layouts (see its setup.typoscript)

Extensions for backend layouts 

In many cases besides defining fixed backend layouts a more modular approach with the possibility of combining different backend layouts and frontend layouts may be feasible. The extension b13/container integrates the grid layout concept also to regular content elements.

The extension ichhabrecht/content-defender offers advanced options to the column positions i.e. allowed or disallowed content elements, a maximal number of content elements.

Backend routing 

Each request to the backend is eventually executed by a controller. A list of routes is defined which maps a given request to a controller and an action.

Routes are defined inside extensions, in the files

Here is an extract of EXT:backend/Configuration/Backend/Routes.php (GitHub):

EXT:backend/Configuration/Backend/Routes.php (excerpt)
<?php

use TYPO3\CMS\Backend\Controller;

return [
    // Login screen of the TYPO3 Backend
    'login' => [
        'path' => '/login',
        'access' => 'public',
        'target' => Controller\LoginController::class . '::formAction',
    ],

    // Main backend rendering setup (previously called backend.php) for the TYPO3 Backend
    'main' => [
        'path' => '/main',
        'referrer' => 'required,refresh-always',
        'target' => Controller\BackendController::class . '::mainAction',
    ],

    // ...
];
Copied!

So, a route file essentially returns an array containing route mappings. A route is defined by a key, a path, a referrer and a target. The "public" access property indicates that no authentication is required for that action.

Backend routing and cross-site scripting 

Public backend routes (those having option 'access' => 'public') do not require any session token, but can be used to redirect to a route that requires a session token internally. For this context, the backend user logged in must have a valid session.

This scenario can lead to situations where an existing cross-site scripting vulnerability (XSS) bypasses the mentioned session token, which can be considered cross-site request forgery (CSRF). The difference in terminology is that this scenario occurs on same-site requests and not cross-site - however, potential security implications are still the same.

Backend routes can enforce the existence of an HTTP referrer header by adding a referrer to routes to mitigate the described scenario.

'main' => [
    'path' => '/main',
    'referrer' => 'required,refresh-empty',
    'target' => Controller\BackendController::class . '::mainAction'
],
Copied!

Values for referrer are declared as a comma-separated list:

  • required enforces existence of HTTP Referer header that has to match the currently used backend URL (for example, https://example.org/typo3/), the request will be denied otherwise.
  • refresh-empty triggers an HTML-based refresh in case HTTP Referer header is not given or empty - this attempt uses an HTML refresh, since regular HTTP Location redirect still would not set a referrer. It implies this technique should only be used on plain HTML responses and will not have any impact, for example, on JSON or XML response types.

This technique should be used on all public routes (without session token) that internally redirect to a restricted route (having a session token). The goal is to protect and keep information about the current session token internal.

The request sequence in the TYPO3 Core looks like this:

  1. HTTP request to https://example.org/typo3/ having a valid user session
  2. Internally public backend route /login is processed
  3. Internally redirects to restricted backend route /main since an existing and valid backend user session was found + HTTP redirect to https://example.org/typo3/main?token=... + exposing the token is mitigated with referrer route option mentioned above

Dynamic URL parts in backend URLs 

New in version 12.1

Backend routes can be registered with path segments that contain dynamic parts, which are then resolved into a PSR-7 request attribute called routing.

These routes are defined within the route path as named placeholders:

EXT:my_extension/Configuration/Backend/Routes.php
<?php

use MyVendor\MyExtension\Controller\MyRouteController;

return [
    'my_route' => [
        'path' => '/my-route/{identifier}',
        'target' => MyRouteController::class . '::handle',
    ],
];
Copied!

Within a controller (we use here a non-Extbase controller as example):

EXT:my_extension/Classes/Controller/MyRouteController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class MyRouteController
{
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $routing = $request->getAttribute('routing');
        $myIdentifier = $routing['identifier'];
        $route = $routing->getRoute();
        // ...
    }
}
Copied!

Generating backend URLs 

Using the UriBuilder API, you can generate any kind of URL for the backend, may it be a module, a typical route or an Ajax call. Therefore use either buildUriFromRoute() or buildUriFromRoutePath(). The UriBuilder then returns a PSR-7 conform Uri object that can be cast to a string when needed. Furthermore, the UriBuilder automatically generates and applies the mentioned session token.

To generate a backend URL via the UriBuilder you'd usually use the route identifier and optional parameters.

In case of Extbase controllers you can append the controller action to the route identifier to directly target those actions. See also module configuration: controllerActions.

Via Fluid ViewHelper 

To generate a backend URL in Fluid you can simply use html:<f:be.link> (which is using UriBuilder internally).

<f:be.link route="web_layout" parameters="{id:42}">go to page 42</f:be.link>
<f:be.link route="web_ExtkeyExample">go to custom BE module</f:be.link>
<f:be.link route="web_ExtkeyExample.MyModuleController_list">
    go to custom BE module but specific controller action
</f:be.link>
Copied!

Via PHP 

Example within a controller (we use here a non-Extbase controller):

EXT:my_extension/Classes/Controller/MyRouteController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;

final class MyRouteController
{
    public function __construct(
        private readonly UriBuilder $uriBuilder,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // ... do some stuff

        // Using a route identifier
        $uri = $this->uriBuilder->buildUriFromRoute(
            'web_layout',
            ['id' => 42],
        );

        // Using a route path
        $uri = $this->uriBuilder->buildUriFromRoutePath(
            '/record/edit',
            [
                'edit' => [
                    'pages' => [
                        123 => 'edit',
                    ],
                ],
            ],
        );

        // Using a PSR-7 request object
        // This is typically useful when linking to the current route or module
        // in the TYPO3 backend to avoid internals with any PSR-7 request attribute.
        $uri = $this->uriBuilder->buildUriFromRequest($request, ['id' => 42]);

        // ... do some other stuff
    }
}
Copied!

New in version 13.0

The UriBuilder->buildUriFromRequest() method has been introduced.

Sudo mode 

The sudo mode, as known from the install tool, can be request for arbitrary backend modules.

You can configure the sudo mode in your backend routing like this:

EXT:my_extension/Configuration/Backend/Routes.php
<?php

use MyVendor\MyExtension\Handlers\MyHandler;
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime;

return [
    'my-route' => [
        'path' => '/my/route',
        'target' => MyHandler::class . '::process',
        'sudoMode' => [
            'group' => 'mySudoModeGroup',
            'lifetime' => AccessLifetime::S,
        ],
    ],
];
Copied!

See also Custom backend modules requiring the sudo mode.

More information 

Please refer to the following resources and look at how the TYPO3 source code handles backend routing in your TYPO3 version.

Backend user object 

The backend user of a session is always available in extensions as the global variable $GLOBALS['BE_USER']. The object is created in \TYPO3\CMS\Backend\Middleware\BackendUserAuthenticator middleware for a standard web request and is an instance of the class \TYPO3\CMS\Core\Authentication\BackendUserAuthentication (which extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication ).

When working with CLI and commands you might initialize the backend user object with \TYPO3\CMS\Core\Core\Bootstrap::initializeBackendUser(). In addition, you can call \TYPO3\CMS\Core\Core\Bootstrap::initializeBackendAuthentication() to load the language of the CLI user set in the backend so that view helpers (like f:translate()) used in the CLI resolve to the correct language.

Checking user access 

The $GLOBALS['BE_USER'] object is mostly used to check user access right, but contains other helpful information. This is presented here by a few examples:

Checking access to any backend module 

If you know the module key you can check if the module is included in the access list by this function call:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('modules', 'web_list');
Copied!

Here access to the module Web > List is checked.

Access to tables and fields? 

The same function ->check() can actually check all the group-based permissions inside $GLOBALS['BE_USER']. For instance:

Checking modify access to the table "pages":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_modify', 'pages');
Copied!

Checking read access to the table "tt_content":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->check('tables_select', 'tt_content');
Copied!

Checking if a table/field pair is allowed explicitly through the "Allowed Excludefields":

EXT:some_extension/Classes/Controller/SomeController.php
$GLOBALS['BE_USER']->check('non_exclude_fields', $table . ':' . $field);
Copied!

Is "admin"? 

If you want to know if a user is an "admin" user (has complete access), just call this method:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->isAdmin();
Copied!

Read access to a page? 

This function call will return true if the user has read access to a page (represented by its database record, $pageRec):

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->doesUserHaveAccess($pageRec, 1);
Copied!

Changing the "1" for other values will check other permissions:

  • use "2" for checking if the user may edit the page
  • use "4" for checking if the user may delete the page.

Is a page inside a DB mount? 

Access to a page should not be checked only based on page permissions but also if a page is found within a DB mount for ther user. This can be checked by this function call ( $id is the page uid):

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->isInWebMount($id)
Copied!

Selecting readable pages from database? 

If you wish to make a SQL statement which selects pages from the database and you want it to be only pages that the user has read access to, you can have a proper WHERE clause returned by this function call:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->getPagePermsClause(1);
Copied!

Again the number "1" represents the "read" permission; "2" is "edit" and "4" is "delete" permission. The result from the above query could be this string:

Result of the above query
((pages.perms_everybody & 1 = 1)OR(pages.perms_userid = 2 AND pages.perms_user & 1 = 1)OR(pages.perms_groupid in (1) AND pages.perms_group & 1 = 1))
Copied!

Saving module data 

This stores the input variable $compareFlags (an array!, retrieved from the request object) with the key "tools_beuser/index.php/compare":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$compareFlags = $request->getParsedBody()['compareFlags'])
    ?? $request->getQueryParams()['compareFlags'])
    ?? null;
$GLOBALS['BE_USER']->pushModuleData('tools_beuser/index.php/compare', $compareFlags);
Copied!

Getting module data 

This gets the module data with the key "tools_beuser/index.php/compare" (lasting only for the session) :

EXT:some_extension/Classes/Controller/SomeModuleController.php
$compareFlags = $GLOBALS['BE_USER']->getModuleData('tools_beuser/index.php/compare', 'ses');
Copied!

Getting TSconfig 

This function can return a value from the "user TSconfig" structure of the user. In this case the value for "options.clipboardNumberPads":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$tsconfig = $GLOBALS['BE_USER']->getTSConfig();
$clipboardNumberPads = $tsconfig['options.']['clipboardNumberPads'] ?? '';
Copied!

Getting the Username 

The full "be_users" record of a authenticated user is available in $GLOBALS['BE_USER']->user as an array. This will return the "username":

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->user['username']
Copied!

Get User Configuration Value 

The internal ->uc array contains options which are managed by the User Tools > User Settings module (extension "setup"). These values are accessible in the $GLOBALS['BE_USER']->uc array. This will return the current state of "Notify me by email, when somebody logs in from my account" for the user:

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->uc['emailMeAtLogin']
Copied!

Broadcast channels 

It is possible to send broadcast messages from anywhere in TYPO3 that are listened to via JavaScript.

Send a message 

Changed in version 12.0

The RequireJS module TYPO3/CMS/Backend/BroadcastService has been migrated to the ES6 module @typo3/backend/broadcast-service.js. See also ES6 in the TYPO3 Backend.

Any backend module may send a message using the @typo3/backend/broadcast-service.js ES6 module.

The payload of such a message is an object that consists at least of the following properties:

  • componentName - the name of the component that sends the message (e.g. extension name)
  • eventName - the event name used to identify the message

A message may contain any other property as necessary. The final event name to listen is a composition of "typo3", the component name and the event name, e.g. typo3:my_extension:my_event.

To send a message, the post() method must be used.

Example code:

EXT:my_broadcast_extension/Resources/Public/JavaScript/my-broadcast-service.js
import BroadcastService from "@typo3/backend/broadcast-service.js";

class MyBroadcastService {
  constructor() {
    const payload = {
      componentName: 'my_extension',
      eventName: 'my_event',
      hello: 'world',
      foo: ['bar', 'baz']
    };
    BroadcastService.post(payload);
  }
}
export default new MyBroadcastService();
Copied!

Receive a message 

To receive and thus react on a message, an event handler needs to be registered that listens to the composed event name (e.g. typo3:my_component:my_event) sent to document.

The event itself contains a property called detail excluding the component name and event name.

Example code:

EXT:my_extension/Resources/Public/JavaScript/my-event-handler.js
class MyEventHandler {
  constructor() {
    document.addEventListener('typo3:my_component:my_event', (e) => eventHandler(e.detail));
  }

  function eventHandler(detail) {
    console.log(detail); // contains 'hello' and 'foo' as sent in the payload
  }
}
export default new MyEventHandler();
Copied!

Hook into $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess'] to load a custom \TYPO3\CMS\Backend\Controller\BackendController hook that loads the event handler's JavaScript.

Example code:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess'][]
    = \MyVendor\MyExtension\Hooks\BackendControllerHook::class . '->registerClientSideEventHandler';
Copied!
EXT:my_extension/Classes/Hooks/BackendControllerHook.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Hooks;

use TYPO3\CMS\Core\Page\PageRenderer;

final class BackendControllerHook
{
    public function __construct(
        private readonly PageRenderer $pageRenderer,
    ) {}

    public function registerClientSideEventHandler(): void
    {
        $this->pageRenderer->loadJavaScriptModule(
            '@myvendor/my-extension/event-handler.js',
        );
        $this->pageRenderer->addInlineLanguageLabelFile(
            'EXT:my_extension/Resources/Private/Language/locallang_slug_service.xlf',
        );
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  MyVendor\MyExtension\Hooks\BackendControllerHook:
    public: true
Copied!

See also: What to make public?

Button components 

The button components are used in the DocHeader of a backend module.

Example on how to use a button component:

EXT:my_extension/Classes/Controller/MyBackendController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\Buttons\DropDown\DropDownItem;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyBackendController
{
    private ModuleTemplate $moduleTemplate;

    public function __construct(
        protected readonly ModuleTemplateFactory $moduleTemplateFactory,
        protected readonly IconFactory $iconFactory,
        // ...
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
        $this->setDocHeader();
        // ... some more logic
    }

    private function setDocHeader(): void
    {
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
        $dropDownButton = $buttonBar->makeDropDownButton()
            ->setLabel('Dropdown')
            ->setTitle('Save')
            ->setIcon($this->iconFactory->getIcon('actions-heart'))
            ->addItem(
                GeneralUtility::makeInstance(DropDownItem::class)
                    ->setLabel('Item')
                    ->setHref('#'),
            );
        $buttonBar->addButton(
            $dropDownButton,
            ButtonBar::BUTTON_POSITION_RIGHT,
            2,
        );
    }
}
Copied!

Generic button component 

New in version 12.2

The component \TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton allows to render any markup in the module menu bar.

Example:

EXT:my_extension/Classes/Controller/MyBackendController.php
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
$genericButton = GeneralUtility::makeInstance(GenericButton::class)
    ->setTag('a')
    ->setHref('#')
    ->setLabel('My label')
    ->setTitle('My title')
    ->setIcon($this->iconFactory->getIcon('actions-heart'))
    ->setAttributes(['data-value' => '123']);
$buttonBar->addButton($genericButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
Copied!

Clipboard 

You can easily access the internal clipboard in TYPO3 from your backend modules:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Utility\DebugUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function debugClipboard()
    {
        /** @var $clipboard Clipboard */
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        // Read the clipboard content from the user session
        $clipboard->initializeClipboard();
        DebugUtility::debug($clipboard->clipData);
    }
}
Copied!

In this simple piece of code we instantiate a clipboard object and make it load its content. We then dump this content into the BE module's debug window, with the following result:

A dump of the clipboard in the debug window

This tells us what objects are registered on the default tab ("normal") (a content element with id 216 in "copy" mode) and the numeric tabs (which can each contain more than one element). It also tells us that the current tab is number 2. We can compare with the BE view of the clipboard:

The clipboard as seen in the backend

which indeed contains two files.

Clipboard content should not be accessed directly, but using the elFromTable() method of the clipboard object:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function getCurrentClipboard():array
    {
        /** @var $clipboard Clipboard */
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        // Read the clipboard content from the user session
        $clipboard->initializeClipboard();
        // Access files and pages content of current pad
        $clipboardContent = [
            'files' => $clipboard->elFromTable('_FILE'),
            'pages' => $clipboard->elFromTable('pages'),
        ];
        return $clipboardContent;
    }
}
Copied!

Here we first try to get all files and then all page records on the current pad (which is pad 2). Then we change to the "Normal" pad, call the elFromTable() method again.

In the "examples" extension, this data is passed to a BE module view for display, which is really just information:

Clipboard items

Context-sensitive menus 

Contextual menus exist in many places in the TYPO3 backend. Just try your luck clicking on any icon that you see. Chances are good that a contextual menu will appear, offering useful functions to execute.

The context menu now contains an additional item "Hello World"

The context menu shown after clicking on the Content Element icon

Context menu rendering flow 

Markup 

Changed in version 13.0

The configuration of the context menu was streamlined. Replace

  • class="t3js-contextmenutrigger" with data-contextmenu-trigger="click"
  • data-table="pages" with data-contextmenu-table="pages"
  • data-uid="10" with data-contextmenu-uid="10"
  • data-context="tree" with data-contextmenu-context="tree"

to be compatible with TYPO3 v12+.

New in version 12.1

The context menu JavaScript API was adapted to also support opening the menu through the "contextmenu" event type (right click) only.

The context menu is shown after clicking on the HTML element which has the data-contextmenu-trigger attribute set together with data-contextmenu-table, data-contextmenu-uid and optional data-contextmenu-context attributes.

The HTML attribute data-contextmenu-trigger has the following options:

  • click: Opens the context menu on the "click" and "contextmenu" events
  • contextmenu: Opens the context menu only on the "contextmenu" event

The JavaScript click event handler is implemented in the ES6 module @typo3/backend/context-menu.js. It takes the data attributes mentioned above and executes an Ajax call to the \TYPO3\CMS\Backend\Controller\ContextMenuController->getContextMenuAction().

Changed in version 12.0

The RequireJS module TYPO3/CMS/Backend/ContextMenu has been migrated to the ES6 module @typo3/backend/context-menu.js. See also ES6 in the TYPO3 Backend.

ContextMenuController 

ContextMenuController asks \TYPO3\CMS\Backend\ContextMenu\ContextMenu to generate an array of items. ContextMenu builds a list of available item providers by asking each whether it can provide items ( $provider->canHandle()), and what priority it has ( $provider->getPriority()).

Item providers registration 

Changed in version 12.0

ContextMenu item providers, implementing \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface are now automatically registered. The registration via $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'] is not evaluated anymore.

Custom item providers must implement \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can extend \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider .

They are then automatically registered by adding the backend.contextmenu.itemprovider tag, if autoconfigure is enabled in Services.yaml. The class \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ItemProvidersRegistry then automatically receives those services and registers them.

If autoconfigure is not enabled in your Configuration/Services.(yaml|php) file, manually configure your item providers with the backend.contextmenu.itemprovider tag.

There are two item providers which are always available:

  • \TYPO3\CMS\Backend\ContextMenu\ItemProviders\PageProvider
  • \TYPO3\CMS\Backend\ContextMenu\ItemProviders\RecordProvider

Gathering items 

A list of providers is sorted by priority, and then each provider is asked to add items. The generated array of items is passed from an item provider with higher priority to a provider with lower priority.

After that, a compiled list of items is returned to the ContextMenuController which passes it back to the ContextMenu.js as JSON.

API usage in the Core 

Several TYPO3 Core modules are already using this API for adding or modifying items. See following places for a reference:

  • EXT:impexp module adds import and export options for pages, content elements and other records. See item provider \TYPO3\CMS\Impexp\ContextMenu\ItemProvider and ES6 module @typo3/impexp/context-menu-actions.js.
  • EXT:filelist module provides several item providers for files, folders, file mounts, filestorage, and drag-drop context menu for the folder tree. See following item providers: \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileDragProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider , \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileStorageProvider, \TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FilemountsProvider and the ES6 module @typo3/filelist/context-menu-actions.js

Adding context menu to elements in your backend module 

Enabling context menu in your own backend modules is quite straightforward. The examples below are taken from the "beuser" system extension and assume that the module is Extbase-based.

The first step is to include the needed JavaScript using the includeJavaScriptModules property of the standard backend container Fluid view helper (or backend page renderer view helper).

Doing so in your layout is sufficient (see typo3/sysext/beuser/Resources/Private/Layouts/Default.html).

<!-- TYPO3 v12 and above -->
<f:be.pageRenderer includeJavaScriptModules="{0: '@typo3/backend/context-menu.js'}">
    // ...
</f:be.pageRenderer>

<!-- TYPO3 v11 and v12 -->
<f:be.pageRenderer
    includeRequireJsModules="{0:'TYPO3/CMS/Backend/ContextMenu'}">
    // ...
</f:be.pageRenderer>
Copied!

The second step is to activate the context menu on the icons. This kind of markup is required (taken from typo3/sysext/beuser/Resources/Private/Templates/BackendUser/Index.html):

<td>
    <a href="#"
        data-contextmenu-trigger="click"
        data-contextmenu-table="be_users"
        data-contextmenu-uid="{compareUser.uid}"
        title="id={compareUser.uid}"
    >
        <be:avatar backendUser="{compareUser.uid}" showIcon="TRUE" />
    </a>
</td>
Copied!

the relevant line being highlighted. The attribute data-contextmenu-trigger triggers a context menu functionality for the current element. The data-contextmenu-table attribute contains a table name of the record and data-contextmenu-uid the uid of the record.

The attribute data-contextmenu-trigger has the following options:

  • click: Opens the context menu on the "click" and "contextmenu" events
  • contextmenu: Opens the context menu only on the "contextmenu" event

One additional data attribute can be used data-contextmenu-context with values being, for example, tree for context menu triggered from the page tree. Context is used to hide menu items independently for page tree independently from other places (disabled items can be configured in TSconfig).

Disabling Context Menu Items from TSConfig 

Context menu items can be disabled in TSConfig by adding item name to the options.contextMenu option corresponding to the table and context you want to cover.

For example, disabling edit and new items for table pages use:

options.contextMenu.table.pages.disableItems = edit,new
Copied!

If you want to disable the items just for certain context (for example tree) add the .tree key after table name like that:

options.contextMenu.table.pages.tree.disableItems = edit,new
Copied!

If configuration for certain context is available, the default configuration is not taken into account.

For more details see TSConfig reference.

Tutorial: How to add a custom context menu item 

Follow these steps to add a custom menu item for pages records. You will add a "Hello world" item which will show an info after clicking.

The context menu now contains an additional item "Hello World"

Context menu with custom item

Step 1: Implementation of the item provider class 

Implement your own item provider class. Provider must implement \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface and can extend \TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider or any other provider from EXT:backend.

See comments in the following code snippet clarifying implementation details.

EXT:examples/Classes/ContextMenu/HelloWorldItemProvider.php
<?php

namespace T3docs\Examples\ContextMenu;

/**
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;

/**
 * Item provider adding Hello World item
 */
class HelloWorldItemProvider extends AbstractProvider
{
    /**
     * This array contains configuration for items you want to add
     * @var array
     */
    protected $itemsConfiguration = [
        'hello' => [
            'type' => 'item',
            'label' => 'Hello World', // you can use "LLL:" syntax here
            'iconIdentifier' => 'actions-lightbulb-on',
            'callbackAction' => 'helloWorld', //name of the function in the JS file
        ],
    ];

    /**
     * Checks if this provider may be called to provide the list of context menu items for given table.
     *
     * @return bool
     */
    public function canHandle(): bool
    {
        // Current table is: $this->table
        // Current UID is: $this->identifier
//        return $this->table === 'pages';
        return true;
    }

    /**
     * Returns the provider priority which is used for determining the order in which providers are processing items
     * to the result array. Highest priority means provider is evaluated first.
     *
     * This item provider should be called after PageProvider which has priority 100.
     *
     * BEWARE: Returned priority should logically not clash with another provider.
     *         Please check @see \TYPO3\CMS\Backend\ContextMenu\ContextMenu::getAvailableProviders() if needed.
     *
     * @return int
     */
    public function getPriority(): int
    {
        return 55;
    }

    /**
     * Registers the additional JavaScript RequireJS callback-module which will allow to display a notification
     * whenever the user tries to click on the "Hello World" item.
     * The method is called from AbstractProvider::prepareItems() for each context menu item.
     *
     * @param string $itemName
     * @return array
     */
    protected function getAdditionalAttributes(string $itemName): array
    {
        return [
            'data-callback-module' => '@t3docs/examples/context-menu-actions',
            // Here you can also add any other useful "data-" attribute you'd like to use in your JavaScript (e.g. localized messages)
        ];
    }

    /**
     * This method adds custom item to list of items generated by item providers with higher priority value (PageProvider)
     * You could also modify existing items here.
     * The new item is added after the 'info' item.
     *
     * @param array $items
     * @return array
     */
    public function addItems(array $items): array
    {
        $this->initDisabledItems();
        // renders an item based on the configuration from $this->itemsConfiguration
        $localItems = $this->prepareItems($this->itemsConfiguration);

        if (isset($items['info'])) {
            //finds a position of the item after which 'hello' item should be added
            $position = array_search('info', array_keys($items), true);

            //slices array into two parts
            $beginning = array_slice($items, 0, $position+1, true);
            $end = array_slice($items, $position, null, true);

            // adds custom item in the correct position
            $items = $beginning + $localItems + $end;
        } else {
            $items = $items + $localItems;
        }
        //passes array of items to the next item provider
        return $items;
    }

    /**
     * This method is called for each item this provider adds and checks if given item can be added
     *
     * @param string $itemName
     * @param string $type
     * @return bool
     */
    protected function canRender(string $itemName, string $type): bool
    {
        // checking if item is disabled through TSConfig
        if (in_array($itemName, $this->disabledItems, true)) {
            return false;
        }
        $canRender = false;
        switch ($itemName) {
            case 'hello':
                $canRender = $this->canSayHello();
                break;
        }
        return $canRender;
    }

    /**
     * Helper method implementing e.g. access check for certain item
     *
     * @return bool
     */
    protected function canSayHello(): bool
    {
        //usually here you can find more sophisticated condition. See e.g. PageProvider::canBeEdited()
        return true;
    }
}
Copied!

Step 2: JavaScript actions 

Provide a JavaScript file (ES6 module) which will be called after clicking on the context menu item.

EXT:examples/Resources/Public/JavaScript/context-menu-actions.js
/**
 * Module: @t3docs/examples/context-menu-actions
 *
 * JavaScript to handle the click action of the "Hello World" context menu item
 */

class ContextMenuActions {

	helloWorld(table, uid) {
		if (table === 'pages') {
			//If needed, you can access other 'data' attributes here from $(this).data('someKey')
			//see item provider getAdditionalAttributes method to see how to pass custom data attributes
			top.TYPO3.Notification.error('Hello World', 'Hi there!', 5);
		}
	};
}

export default new ContextMenuActions();
Copied!

Register the JavaScript ES6 modules of your extension if not done yet:

examples/Configuration/JavaScriptModules.php
<?php

return [
    'dependencies' => ['core', 'backend'],
    'tags' => [
        'backend.contextmenu',
    ],
    'imports' => [
        '@t3docs/examples/' => 'EXT:examples/Resources/Public/JavaScript/',
    ],
];
Copied!

Step 3: Registration 

If you have autoconfigure: true set in your extension's Services.yaml file all classes implementing \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface get registered as context menu items automatically:

EXT:examples/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false
Copied!

If autoconfigure is disabled you can manually register a context menu item provider by adding the tag backend.contextmenu.itemprovider:

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

  MyVendor\MyExtension\ContextMenu\SomeItemProvider:
    tags:
      - name: backend.contextmenu.itemprovider
Copied!

Using Custom Permission Options 

TYPO3 allows extension developers to register their own permission options, managed automatically by the built-in user group access lists. The options can be grouped in categories. A custom permission option is always a checkbox (on/off).

The scope of such options is the backend only.

Registration 

Options are configured in the global variable $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'] in EXT:my_extension/ext_tables.php. The syntax is demonstrated in the following example, which registers two custom permission options:

EXT:my_extension/ext_tables.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

// Register some custom permission options shown in BE group access lists
$GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']['tx_styleguide_custom'] = [
    'header' => 'Custom styleguide permissions',
    'items' => [
        'key1' => [
            'Option 1',
            // Icon has been registered in Icons.php
            'tcarecords-tx_styleguide_forms-default',
            'Description 1',
        ],
        'key2' => [
            'Option 2',
        ],
    ],
];
Copied!

The result is that these options appear in the group access lists like this:

The custom permissions appear in the Access List tab of backend user groups

As you can see it is possible to add both an icon and a description text. If icons not provided by the Core are used, they need to be registered with the Icon API.

Evaluation 

To check if a custom permission option is set call the following API function from the user object:

EXT:some_extension/Classes/SomeClass.php
$GLOBALS['BE_USER']->check('custom_options', $catKey . ':' . $itemKey);
Copied!

$catKey is the category in which the option resides. From the example above this would be tx_examples_cat1.

$itemKey is the key of the item in the category you are evaluating. From the example above this could be key1, key2 or key3 depending on which one of them you want to evaluate.

The function returns true if the option is set, otherwise false.

Keys for Options 

It is good practice to use the extension keys prefixed with tx_ on the first level of the array to avoid potential conflicts with other custom options.

Backend login form API 

Registering a login provider 

The concept of the backend login is based on "login providers".

A login provider can be registered within your config/system/settings.php or config/system/additional.php like this:

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'][1433416020] = [
    'provider' => \MyVendor\MyExtension\LoginProvider\CustomLoginProvider::class,
    'sorting' => 50,
    'iconIdentifier' => 'actions-key',
    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:login.link'
];
Copied!

The settings are defined as:

provider
The login provider class name, which must implement \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface .
sorting
The sorting is important for the ordering of the links to the possible login providers on the login screen.
iconIdentifier
Accepts any icon identifier that is available in the Icon Registry.
label
The label for the login provider link on the login screen.

For a new login provider you have to register a new key - by best practice the current unix timestamp - in $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'] .

If your login provider extends another one, you may only overwrite necessary settings. An example would be to extend an existing provider and replace its registered provider class with your custom class.

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'][1433416020]['provider'] =
    \MyVendor\MyExtension\LoginProvider\CustomProviderExtendingUsernamePasswordLoginProvider::class
Copied!

LoginProviderInterface 

Deprecated since version 13.3

Method LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14. See section Migration.

The LoginProviderInterface contains only the deprecated render() method in TYPO3 v13.

interface LoginProviderInterface
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface

Interface for Backend Login providers

render ( \TYPO3\CMS\Fluid\View\StandaloneView $view, \TYPO3\CMS\Core\Page\PageRenderer $pageRenderer, \TYPO3\CMS\Backend\Controller\LoginController $loginController)

Render the login HTML

Implement this method and set the template for your form. This is also the right place to assign data to the view and add necessary JavaScript resources to the page renderer.

A good example is EXT:openid

Example:
$view->setTemplatePathAndFilename($pathAndFilename); $view->assign('foo', 'bar');
param $view

the view

param $pageRenderer

the pageRenderer

param $loginController

the loginController

Migration 

Consumers of LoginProviderInterface should implement the modifyView() method and and retain a stub for the render() method to satisfy the interface. See the example below.

The transition should be smooth. Consumers that need \TYPO3\CMS\Core\Page\PageRenderer for JavaScript magic, should use dependency injection to receive an instance of it.

An implementation of LoginProviderInterface could look like this for TYPO3 v13:

EXT:my_extension/Classes/Login/MyLoginProvider.php
<?php

namespace MyVendor\MyExtension\Login;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Controller\LoginController;
use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\View\ViewInterface;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
use TYPO3\CMS\Fluid\View\StandaloneView;

#[Autoconfigure(public: true)]
final readonly class MyLoginProvider implements LoginProviderInterface
{
    public function __construct(
        private PageRenderer $pageRenderer,
    ) {}

    /**
     * todo: Remove when dropping TYPO3 v13 support
     * @deprecated Remove in v14 when method is removed from LoginProviderInterface
     */
    public function render(StandaloneView $view, PageRenderer $pageRenderer, LoginController $loginController)
    {
        throw new \RuntimeException('Legacy interface implementation. Should not be called', 123456789);
    }

    public function modifyView(ServerRequestInterface $request, ViewInterface $view): string
    {
        $this->pageRenderer->addJsFile('someFile');
        // Custom login provider implementations can add custom fluid lookup paths.
        if ($view instanceof FluidViewAdapter) {
            $templatePaths = $view->getRenderingContext()->getTemplatePaths();
            $templateRootPaths = $templatePaths->getTemplateRootPaths();
            $templateRootPaths[] = 'EXT:my_extension/Resources/Private/Templates';
            $templatePaths->setTemplateRootPaths($templateRootPaths);
        }
        $view->assign('Some Variable', 'some value');
        return 'Login/MyLoginForm';
    }
}
Copied!

The default implementation in UsernamePasswordLoginProvider is a good example. Extensions that need to configure additional template, layout or partial lookup paths can extend them, see lines 23-28 in the example above.

Consumers of ModifyPageLayoutOnLoginProviderSelectionEvent should use the request instead, and/or should get an instance of PageRenderer injected as well.

The view 

Deprecated since version 13.3

Method LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14. See section Migration.

The name of the template must be returned by the modifyView() method of the login provider. Variables can be assigned to the view supplied as second parameter.

The template file must only contain the form fields, not the form-tag. Later on, the view renders the complete login screen.

View requirements:

  • The template must use the Login-layout provided by the Core <f:layout name="Login">.
  • Form fields must be provided within the section <f:section name="loginFormFields">.
EXT:my_sitepackage/Resources/Private/Templates/MyLoginForm.html
<f:layout name="Login" />
<f:section name="loginFormFields">
    <div class="form-group t3js-login-openid-section" id="t3-login-openid_url-section">
        <div class="input-group">
            <input
                type="text"
                id="openid_url"
                name="openid_url"
                value="{presetOpenId}"
                autofocus="autofocus"
                placeholder="{f:translate(key: 'openId', extensionName: 'openid')}"
                class="form-control input-login t3js-clearable t3js-login-openid-field"
            >
            <div class="input-group-addon">
                <span class="fa fa-openid"></span>
            </div>
        </div>
    </div>
</f:section>
Copied!

Examples 

Within the Core you can find the standard implementation in the system extension backend:

See class EXT:backend/Classes/LoginProvider/UsernamePasswordLoginProvider.php (GitHub) with its template EXT:backend/Resources/Private/Templates/Login/UserPassLoginForm.html (GitHub).

The page tree in the TYPO3 backend 

The page tree is a hierarchical structure that represents pages and their subpages on a website, allowing editors and integrators to easily organize and manage content and navigation.

It is displayed on the left of backend modules with navigationComponent set to '@typo3/backend/tree/page-tree-element'.

Usage of the page tree is described in Getting Started Tutorial, Page tree.

The page tree can also be navigated via Keyboard commands (Tutorial for Editors).

PSR-14 events to influence the functionality of the page tree 

AfterPageTreeItemsPreparedEvent
Allows prepared page tree items to be modified.
AfterRawPageRowPreparedEvent
Allows to modify the populated properties of a page and children records before the page is displayed in a page tree.

TsConfig settings to influence the page tree 

The rendering of the page tree can be influenced via user TsConfig options.pageTree.

Bitsets & Enumerations 

  • Use an enumeration, if you have a fixed list of values.
  • Use a bitset, if you have a list of boolean flags.

Do not use PHP constants directly, if your code is meant to be extendable, as constants cannot be deprecated, but the values of an enumeration or methods of a bitset can.

Background and history 

Before version 8.1, PHP had no enumeration concept as part of the language. Therefore the TYPO3 Core includes a custom enumeration implementation.

In TYPO3, enumerations are implemented by extending the abstract class \TYPO3\CMS\Core\Type\Enumeration . It was originally implemented similar to \SplEnum which is unfortunately part of the unmaintained package PECL spl_types.

With PHP version 8.1, an enumeration concept was implemented (see the Enumeration documentation for more details). This makes it possible to drop the custom enumeration concept from the Core in a future TYPO3 version.

How to use enumerations 

Changed in version 13.0

Create an enumeration 

To create a new enumeration you have to extend the class \TYPO3\CMS\Core\Type\Enumeration . Make sure your enumeration is marked as final, this ensures your code only receives a known set of values. Otherwise adding more values by extension will lead to undefined behavior in your code.

Values are defined as constants in your implementation. The names of the constants must be given in uppercase.

A special, optional constant __default represents the default value of your enumeration, if it is present. In that case the enumeration can be instantiated without a value and will be set to the default.

Example:

EXT:my_extension/Classes/Enumeration/LikeWildcard.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumerations;

use TYPO3\CMS\Core\Type\Enumeration;

final class LikeWildcard extends Enumeration
{
    public const __default = self::BOTH;

    /**
     * @var int Do not use any wildcard
     */
    public const NONE = 0;

    /**
     * @var int Use wildcard on left side
     */
    public const LEFT = 1;

    /**
     * @var int Use wildcard on right side
     */
    public const RIGHT = 2;

    /**
     * @var int Use wildcard on both sides
     */
    public const BOTH = 3;
}
Copied!

Use an enumeration 

You can create an instance of the Enumeration class like you would usually do, or you can use the Enumeration::cast() method for instantiation. The Enumeration::cast() method can handle:

  • Enumeration instances (where it will simply return the value) and
  • simple types with a valid Enumeration value,

whereas the "normal" __construct() will always try to create a new instance.

That allows to deprecate enumeration values or do special value casts before finding a suitable value in the enumeration.

Example:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use MyVendor\MyExtension\Enumerations\LikeWildcard;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        $likeWildcardLeft = LikeWildcard::cast(LikeWildcard::LEFT);

        $valueFromDatabase = 1;

        // will cast the value automatically to an enumeration.
        // Result is true.
        $likeWildcardLeft->equals($valueFromDatabase);

        $enumerationWithValueFromDb = LikeWildcard::cast($valueFromDatabase);

        // Remember to always use ::cast and never use the constant directly
        $enumerationWithValueFromDb->equals(LikeWildcard::cast(LikeWildcard::RIGHT));

        // ...
    }

    // ...
}
Copied!

Exceptions 

If the enumeration is instantiated with an invalid value, a \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException is thrown. This exception must be caught, and you have to decide what the appropriate behavior should be.

Example:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use MyVendor\MyExtension\Enumerations\LikeWildcard;
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        try {
            $foo = LikeWildcard::cast($valueFromPageTs);
        } catch (InvalidEnumerationValueException $exception) {
            $foo = LikeWildcard::cast(LikeWildcard::NONE);
        }

        // ...
    }

    // ...
}
Copied!

Implement custom logic 

Sometimes it makes sense to not only validate a value, but also to have custom logic as well.

For example, the \TYPO3\CMS\Core\Versioning\VersionState enumeration contains values of version states. Some of the values indicate that the state is a "placeholder". This logic can be implemented by a custom method:

EXT:core/Classes/Versioning/VersionState.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Type\Enumeration;

final class VersionState extends Enumeration
{
    public const __default = self::DEFAULT_STATE;
    public const DEFAULT_STATE = 0;
    public const NEW_PLACEHOLDER = 1;
    public const DELETE_PLACEHOLDER = 2;

    /**
     * @return bool
     */
    public function indicatesPlaceholder(): bool
    {
        return (int)$this->__toString() > self::DEFAULT_STATE;
    }
}
Copied!

The method can then be used in your class:

EXT:my_extension/Classes/SomeClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

final class SomeClass
{
    public function doSomething()
    {
        // ...

        $myVersionState = VersionState::cast($versionStateValue);
        if ($myVersionState->indicatesPlaceholder()) {
            echo 'The state indicates that this is a placeholder';
        }

        // ...
    }

    // ...
}
Copied!

Migration to backed enums 

Class definition:

EXT:my_extension/Classes/Enumeration/State.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumeration;

use TYPO3\CMS\Core\Type\Enumeration;

class State extends Enumeration
{
    public const STATE_DEFAULT = 'somestate';
    public const STATE_DISABLED = 'disabled';
}
Copied!

should be converted into:

EXT:my_extension/Classes/Enumeration/State.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Enumeration;

enum State: string
{
    case STATE_DEFAULT = 'somestate';
    case STATE_DISABLED = 'disabled';
}
Copied!

Existing method calls must be adapted.

How to use bitsets 

Bitsets are used to handle boolean flags efficiently.

The class \TYPO3\CMS\Core\Type\BitSet provides a TYPO3 implementation of a bitset. It can be used standalone and accessed from the outside, but we recommend creating specific bitset classes that extend the TYPO3 BitSet class.

The functionality is best described by an example:

<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Type\BitSet;

define('PERMISSIONS_NONE', 0b0); // 0
define('PERMISSIONS_PAGE_SHOW', 0b1); // 1
define('PERMISSIONS_PAGE_EDIT', 0b10); // 2
define('PERMISSIONS_PAGE_DELETE', 0b100); // 4
define('PERMISSIONS_PAGE_NEW', 0b1000); // 8
define('PERMISSIONS_CONTENT_EDIT', 0b10000); // 16
define('PERMISSIONS_ALL', 0b11111); // 31

$bitSet = new BitSet(PERMISSIONS_PAGE_SHOW | PERMISSIONS_PAGE_NEW);
$bitSet->get(PERMISSIONS_PAGE_SHOW); // true
$bitSet->get(PERMISSIONS_CONTENT_EDIT); // false
Copied!

The example above uses global constants. Implementing that via an extended bitset class makes it clearer and easier to use:

EXT:my_extension/Classes/Bitmask/Permissions.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Bitmask;

use TYPO3\CMS\Core\Type\BitSet;

final class Permissions extends BitSet
{
    public const NONE = 0b0; // 0
    public const PAGE_SHOW = 0b1; // 1
    public const PAGE_EDIT = 0b10; // 2
    public const PAGE_DELETE = 0b100; // 4
    public const PAGE_NEW = 0b1000; // 8
    public const CONTENT_EDIT = 0b10000; // 16
    public const ALL = 0b11111; // 31

    public function hasPermission(int $permission): bool
    {
        return $this->get($permission);
    }

    public function hasAllPermissions(): bool
    {
        return $this->get(self::ALL);
    }

    public function allow(int $permission): void
    {
        $this->set($permission);
    }
}
Copied!

Then use your custom bitset class:

<?php

declare(strict_types=1);

use MyVendor\MyExtension\Bitmask\Permissions;

$permissions = new Permissions(Permissions::PAGE_SHOW | Permissions::PAGE_NEW);
$permissions->hasPermission(Permissions::PAGE_SHOW); // true
$permissions->hasPermission(Permissions::CONTENT_EDIT); // false
Copied!

Caching 

Caching in TYPO3 

TYPO3 uses multiple caching strategies to ensure fast content delivery. Depending on the content a page contains, TYPO3 chooses the best caching strategy for that use case.

For example, you might have a fully-cacheable page, a page that is at least partially cacheable or a page that is completely dynamic. Dynamic elements in TYPO3 are also known as USER_INT or COA_INT objects - as these are the matching Typoscript objects used to render non-cacheable content.

When visiting a TYPO3 web site, TYPO3 knows the following states:

  • first time hit, page has never been rendered ("cache miss")
  • consecutive hit, page has been rendered before ("cache hit")

In the first case, TYPO3 renders the complete page and writes cache entries as configured. In the second case, TYPO3 fetches those entries from the cache and delivers them to the user without re-triggering the rendering.

In that second case, either the page is fully cached and directly delivered from the cache, or the page has non-cacheable elements on it. If a page has non-cacheable elements, TYPO3 first fetches the cached part of the page and then renders all dynamic parts.

Caching variants - or: What is a "cache hash"? 

TYPO3 ideally delivers fully cached pages for maximum performance. However, in scenarios where the same page will deliver different content depending on URL parameters, TYPO3 needs a possibility to identify these "variants" and cache each of them differently. For example, if you have a news plugin and a detail page, the detail page is different for every news entry.

To identify the variant, TYPO3 combines a set of parameters and generates a hash value as identifier. These parameters include by default:

  • id: The current page ID
  • type: The current page type (typeNum)
  • groupIds: The user groups of the logged-in user (if no user is logged in: 0, -1 as default values)
  • MP: The mount point identifier
  • site: The current site and base URL
  • staticRouteArguments: Any route argument configured in the routing configuration and resolved in the current request
  • dynamicArguments: Any "GET" parameters influencing the rendered page

Imagine the following URL https://example.org/news/?tx_example_news[id]=123 displaying the news with ID 123. If TYPO3 would cache that page with that parameter without any security mechanisms, it would open a potential denial of service attack as an unlimited amount of cache entries could be generated by adding arbitrary parameters. To avoid that, TYPO3 generates a so-called "cHash" parameter, which is a hash that basically signs the valid parameters for that request. So any parameter that validly influences the rendered page needs to be part of that cHash.

With routing you can configure TYPO3 not to display the cHash in your URLs in most cases. Routing adds an explicit mapping of incoming readable URL slugs to internal parameter values. This both adds an additional layer for validating slugs as well as reduces the parameters to a limited (and predictable) set of values.

Various configuration options exist to configure the cHash behavior via $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash'] in the file config/system/settings.php or config/system/additional.php:

cachedParametersWhiteList

cachedParametersWhiteList

Only the given parameters will be evaluated in the cHash calculation. Example: tx_news_pi1[uid]

requireCacheHashPresenceParameters

requireCacheHashPresenceParameters

Configure parameters that require a cHash. If no cHash is given, but one of the parameters are set, then TYPO3 triggers the configured cHash error behavior

excludedParameters

excludedParameters

The given parameters will be ignored in the cHash calculation. Example: L,tx_search_pi1[query]

excludedParametersIfEmpty

excludedParametersIfEmpty

Configure parameters only being relevant for the cHash if there is an associated value available. Set excludeAllEmptyParameters to true to skip all empty parameters.

excludeAllEmptyParameters

excludeAllEmptyParameters

If true, all parameters relevant to cHash are only considered when they are not empty.

enforceValidation

enforceValidation

If this option is enabled, the same validation is used to calculate a cHash value as when a valid or invalid "cHash" parameter is given to a request, even when no cHash is given.

All properties can be configured with an array of values. Besides exact matches (equals) it is possible to apply partial matches at the beginning of a parameter (startsWith) or inline occurrences (contains).

URL parameter names are prefixed with the following indicators:

  • = (equals): exact match, default behavior if not given
  • ^ (startsWith): matching the beginning of a parameter name
  • ~ (contains): matching any inline occurrence in a parameter name

These indicators can be used for all previously existing sub-properties cachedParametersWhiteList, excludedParameters, excludedParametersIfEmpty and requireCacheHashPresenceParameters.

Example (excerpt of config/system/additional.php) 

config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash'] = [
    'excludedParameters' => [
        'utm_source',
        'utm_medium',
    ],
    'excludedParametersIfEmpty' => [
        '^tx_my_plugin[aspects]',
        'tx_my_plugin[filter]',
    ],
];
Copied!

For instance instead of having exclude items like

config/system/additional.php | typo3conf/system/additional.php
'excludedParameters' => [
    'tx_my_plugin[data][uid]',
    'tx_my_plugin[data][category]',
    'tx_my_plugin[data][order]',
    'tx_my_plugin[data][origin]',
    // ...
],
Copied!

partial matches allow to simplify the configuration and consider all items having tx_my[data] (or tx_my[data][ to be more specific) as prefix like

config/system/additional.php | typo3conf/system/additional.php
'excludedParameters' => [
    '^tx_my_plugin[data][',
    // ...
],
Copied!

Clearing/flushing and warming up caches 

TYPO3 provides different possibilities to clear all or specific caches. Depending on the context, you might want to

  • clear the cache for a single page (for example, to make changes visible)
  • clear the frontend cache (for example, after templating changes)
  • clear all caches (for example, during development, when making TypoScript changes)
  • clear all caches incl. dependency injection (mainly useful during development)

Clearing the cache for a single page is done by using the "clear cache button" on that page in the backend (usually visualized with a "bolt" icon). Depending on user rights this option is available for editors.

Clearing the frontend caches and all caches can be done via the system toolbar in the TYPO3 backend. This option should only be available to administrators. Clearing all caches can have a significant impact on the performance of the web site and should be used sparingly on production systems.

The "fullest" option to clear all caches can be reached via the System Tools > Maintenance section. In the development context when new classes have been added to the system or in case of problems with the system using this cache clearing option will clear all caches including compiled code like the dependency injection container.

Clear cache command 

In addition to the GUI options explained above, caches can also be cleared via a CLI command:

vendor/bin/typo3 cache:flush
Copied!
typo3/sysext/core/bin/typo3 cache:flush
Copied!

Specific cache groups can be defined via the group option. The usage is described as this:

cache:flush [--group <all|system|di|pages|...>]
Copied!

All available cache groups can be supplied as option. The command defaults to flush all available cache groups as the System Tools > Maintenance area does.

Extensions that register custom caches may listen to the CacheFlushEvent, but usually the cache flush via cache manager groups will suffice to clear those caches, too.

Cache warmup 

Changed in version 13.4.19

It is possible to warmup TYPO3 caches using the command line.

The administrator can use the following CLI command:

vendor/bin/typo3 cache:warmup
Copied!
typo3/sysext/core/bin/typo3 cache:warmup
Copied!

Specific cache groups can be defined via the group option. The usage is described as this:

cache:warmup [--group <all|system|di|pages|...>]
Copied!

All available cache groups can be supplied as option. The command defaults to warm all available cache groups.

Extensions that register custom caches are encouraged to implement cache warmers via CacheWarmupEvent.

Use case: deployment 

It is often required to clear caches during deployment of TYPO3 instance. The integrator may decide to flush all caches or may alternatively flush selected groups (for example, "pages"). It is common practice to clear all caches during deployment of a TYPO3 instance update. This means that the first request after a deployment usually takes a major amount of time and blocks other requests due to cache-locks.

TYPO3 caches can be warmed during deployment in release preparatory steps in symlink-based deployment/release procedures. This enables fast first requests with all (or at least system) caches being prepared and warmed.

Caches are often filesystem relevant (file paths are calculated into cache hashes), therefore cache warmup should only be performed on the the live system, in the final folder of a new release, and ideally before switching to that new release (via symlink switch).

To summarize: Cache warmup is to be used during deployment, on the live system server, inside the new release folder and before switching to the new release.

An example deployment could consist of:

  • Before the release:

    • git-checkout/rsync your codebase to the continuous integration / build server
    • composer install on the continuous integration / build server
    • vendor/bin/typo3 cache:warmup --group system (only on the live system)
  • Change release symlink to the new release folder
  • After the release:

    • vendor/bin/typo3 cache:flush --group pages

The conceptional idea is to warmup all file-related caches before (symlink) switching to a new release and to only flush database and frontend (shared) caches after the symlink switch. Database warmup could be implemented with the help of the CacheWarmupEvent as an additionally functionality by third-party extensions.

Note that file-related caches (summarized into the group "system") can safely be cleared before doing a release switch, as it is recommended to keep file caches per release. In other words, share var/session/, var/log/, var/lock/ and var/charset/ between releases, but keep var/cache/ be associated only with one release.

Caching framework 

TYPO3 contains a data caching framework which supports a wide variety of storage solutions and options for different caching needs. Each cache can be configured individually and can implement its own specific storage strategy.

The caching framework exists to help speeding up TYPO3 sites, especially heavily loaded ones. It is possible to move all caches to a dedicated cache server with specialized cache systems like the Redis key-value store (a so called NoSQL database).

Major parts of the original caching framework were originally backported from TYPO3 Flow.

Quick start for integrators 

This section gives some simple instructions for getting started using the caching framework without going into all the details under the hood.

Change specific cache options 

By default, most Core caches use the database backend. The default cache configuration is defined in EXT:core/Configuration/DefaultConfiguration.php (GitHub) and can be overridden in config/system/settings.php.

If specific settings should be applied to the configuration, they should be added to config/system/settings.php. All settings in config/system/settings.php will be merged with DefaultConfiguration.php. The easiest way to see the final cache configuration is to use the TYPO3 backend module System > Configuration > $GLOBALS['TYPO3_CONF_VARS'] (with installed lowlevel system extension).

Example for a configuration of a Redis cache backend on Redis database number 42 instead of the default database backend with compression for the pages cache:

config/system/settings.php | typo3conf/system/settings.php
<?php

use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

return [
    // ...
    'SYS' => [
        // ...
        'caching' => [
            // ...
            'cacheConfigurations' => [
                // ...
                'pages' => [
                    'backend' => RedisBackend::class,
                    'options' => [
                        'database' => 42,
                    ],
                ],
            ],
        ],
    ],
];
Copied!

Garbage collection task 

Most cache backends do not have an internal system to remove old cache entries that exceeded their lifetime. A cleanup must be triggered externally to find and remove those entries, otherwise caches could grow to arbitrary size. This could lead to a slow website performance, might sum up to significant hard disk or memory usage and could render the server system unusable.

It is advised to always enable the scheduler and run the "Caching framework garbage collection" task to retain clean and small caches. This housekeeping could be done once a day when the system is otherwise mostly idle.

Cache configuration 

Caches are configured in the array $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching'] . The basic structure is predefined in EXT:core/Configuration/DefaultConfiguration.php (GitHub), and consists of the single section:

cacheConfigurations
Registry of all configured caches. Each cache is identified by its array key. Each cache can have the sub-keys frontend, backend and options to configure the used frontend, backend and possible backend options.

Cache configurations 

Unfortunately in TYPO3, all ext_localconf.php files of the extensions are loaded after the instance-specific configuration from config/system/settings.php and config/system/additional.php. This enables extensions to overwrite cache configurations already done for the instance. All extensions should avoid this situation and should define the very bare minimum of cache configurations. This boils down to define the array key to populate a new cache to the system. Without further configuration, the cache system falls back to the default backend and default frontend settings:

EXT:my_extension/ext_localconf.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myext_mycache']
    ??= [];
Copied!

Extensions, like Extbase, define default caches this way, giving administrators full freedom for specific and possibly quicker setups (for example, a memory-driven cache for the Extbase reflection cache).

Administrators can overwrite specific settings of the cache configuration in config/system/settings.php or config/system/additional.php. Here is an example configuration to switch pages to the Redis backend using database 3:

config/system/additional.php | typo3conf/system/additional.php
<?php

use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
    'backend' => RedisBackend::class,
    'options' => [
        'database' => 3,
    ],
];
Copied!

Some backends have mandatory as well as optional parameters (which are documented in the Cache backends section). If not all mandatory options are defined, the specific backend will throw an exception, if accessed.

How to disable specific caches 

During development it can be convenient to disable certain caches. This is especially helpful for central caches like the language or autoloader cache. This can be achieved by using the null backend as storage backend.

Example configuration to switch the extbase_reflection cache to use the null backend:

config/system/additional.php | typo3conf/system/additional.php
<?php

use TYPO3\CMS\Core\Cache\Backend\NullBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']
    ['extbase_reflection']['backend'] = NullBackend::class;
Copied!

Caching framework architecture 

Basic know-how 

The caching framework can handle multiple caches with different configurations. A single cache consists of any number of cache entries.

A single cache entry is defined by these fields:

identifier
A string as unique identifier within this cache. It is used to store and retrieve entries.
data
The data to be cached.
lifetime
A lifetime in seconds of this cache entry. An entry can not be retrieved from cache, if the lifetime is expired.
tags
Additional tags (an array of strings) assigned to the entry. It is used to remove specific cache entries.

About the identifier 

The identifier is used to store ("set") and retrieve ("get") entries from the cache and holds all information to differentiate entries from each other. For performance reasons, it should be quick to calculate.

Suppose a resource-intensive extension is added as a plugin on two different pages. The calculated content depends on the page on which it is inserted and if a user is logged in or not. So, the plugin creates at maximum four different content outputs, which can be cached in four different cache entries:

  • page 1, no user logged in
  • page 1, a user is logged in
  • page 2, no user logged in
  • page 2, a user is logged in

To differentiate all entries from each other, the identifier is built from the page ID where the plugin is located, combined with the information whether a user is logged in. These are concatenated and hashed. In PHP this could look like this:

EXT:my_extension/Classes/SomeClass.php
$identifier = sha1((string)$this->getPageUid() . (string)$this->isUserLoggedIn());
Copied!

When the plugin is accessed, the identifier is calculated early in the program flow. Next, the plugin looks up for a cache entry with this identifier. If such an entry exists, the plugin can return the cached content, else it calculates the content and stores a new cache entry with this identifier.

In general, the identifier is constructed from all dependencies which specify a unique set of data. The identifier should be based on information which already exist in the system at the point of its calculation. In the above scenario the page ID and whether or not a user is logged in are already determined during the frontend bootstrap and can be retrieved from the system quickly.

About tags 

Tags are used to drop specific cache entries when some information they are based on is changed.

Suppose the above plugin displays content based on different news entries. If one news entry is changed in the backend, all cache entries which are compiled from this news row must be dropped to ensure that the frontend renders the plugin content again and does not deliver old content on the next frontend call.

For example, if the plugin uses news number one and two on one page, and news one on another page, the related cache entries should be tagged with these tags:

  • page 1, tags news_1, news_2
  • page 2, tag news_1

If entry 2 is changed, a simple backend logic (probably a hook in DataHandler) could be created, which drops all cache entries tagged with news_2. In this case the first entry would be invalidated while the second entry still exists in the cache after the operation.

While there is always exactly one identifier for each cache entry, an arbitrary number of tags can be assigned to an entry and one specific tag can be assigned to multiple cache entries. All tags a cache entry has are given to the cache when the entry is stored ("set").

Caches in the TYPO3 Core 

The TYPO3 Core defines and uses several caching framework caches by default. This section gives an overview of default caches, its usage and behaviour. If not stated otherwise, the default database backend with variable frontend is used.

The various caches are organized in groups. Currently, the following groups exist:

pages
Frontend-related caches.
system
System caches. Flushing system caches should be avoided as much as possible, as rebuilding them requires significant resources.
lowlevel
Low-level caches. Flushing low-level caches manually should be avoided completely.
all
All other caches.

Cache clearing commands can be issued to target a particular group. If a cache does not belong to a group, it will be flushed when the "all" group is flushed, but such caches should normally be transient anyway.

There are TSconfig options for permissions corresponding to each group.

The following caches exist in the TYPO3 Core:

core

group: system

  • Core cache for compiled PHP code. It should not be used by extensions.
  • Uses the PHP frontend with the Simple file backend for maximum performance.
  • Stores Core internal compiled PHP code like concatenated ext_tables.php and ext_localconf.php files and autoloader.
  • This cache is instantiated very early during bootstrap and can not be re-configured by instance-specific config/system/settings.php or similar.
  • Cache entries are located in directory var/cache/code/core/ (for Composer-based installations) and typo3temp/var/cache/code/core/ (for Classic mode installations). The full directory and any file in this directory can be safely removed and will be re-created upon next request. This is especially useful during development
hash

groups: all, pages

  • Stores several key-value based cache entries, mostly used during frontend rendering.
pages

groups: all, pages

  • The frontend page cache. It stores full frontend pages.
  • The content is compressed by default to reduce database memory and storage overhead.
runtime
  • Runtime cache to store data specific for current request.
  • Used by several Core parts during rendering to re-use already calculated data.
  • Valid for one request only.
  • Can be re-used by extensions that have similar caching needs.
rootline

groups: all, pages

  • Cache for rootline calculations.
  • A quick and simple cache dedicated for Core usage, it should not be re-used by extensions.
assets

groups: system

  • Cache for assets.
  • Examples: backend icons, RTE or JavaScript configuration.
l10n

groups: system

Cache for the localized labels.

fluid_template

groups: system

  • Cache for Fluid templates.
extbase

group: system

  • Contains detailed information about a class' member variables and methods.
ratelimiter

group: system

typoscript

group: pages

database_schema

group: system

Cache for database schema information.

dashboard_rss
  • Contains the contents of RSS feeds retrieved by RSS widgets on the dashboard.
  • This cache can be used by extension authors for their own RSS widgets.

Garbage collection task 

The Core system provides a scheduler task to collect the garbage of all cache backends. This is important for backends like the database backend that do not remove old cache entries and tags internally. It is highly recommended to add this scheduler task and run it once in a while (maybe once a day at night) for all used backends that do not delete entries which exceeded their lifetime on their own to free up memory or hard disk space.

Cache API 

The caching framework architecture is based on the following classes:

\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Main interface to handle cache entries of a specific cache. Different frontends and further interfaces exist to handle different data types.
\TYPO3\CMS\Core\Cache\Backend\BackendInterface
Main interface that every valid storage backend must implement. Several backends and further interfaces exist to specify specific backend capabilities. Some frontends require backends to implement additional interfaces.

Cache frontends 

A cache frontend is the public API for interacting with a cache. It defines which value types are accepted and how they are prepared for storage (for example serialization or compilation), while delegating persistence to the assigned backend. In everyday use, extensions should work with the frontend only — direct access to the caching backend is discouraged.

Caching frontend API 

All caching frontends must implement the interface \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface .

interface FrontendInterface
Fully qualified name
\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface

Contract for a Cache (frontend)

public const TAG_CLASS

'%CLASS%', type string

"Magic" tag for class-related entries

public const TAG_PACKAGE

'%PACKAGE%', type string

"Magic" tag for package-related entries

public const PATTERN_ENTRYIDENTIFIER

'/^[a-zA-Z0-9_%\\-&]{1,250}$/', type string

Pattern an entry identifier must match.

public const PATTERN_TAG

'/^[a-zA-Z0-9_%\\-&]{1,250}$/', type string

Pattern a tag must match.

getIdentifier ( )

Returns this cache's identifier

Return description

The identifier for this cache

Returns
string
getBackend ( )

Returns the backend used by this cache

Return description

The backend used by this cache

Returns
\TYPO3\CMS\Core\Cache\Backend\BackendInterface
set ( ?string $entryIdentifier, ?mixed $data, array $tags = [], ?int $lifetime = NULL)

Saves data in the cache.

param $entryIdentifier

Something which identifies the data - depends on concrete cache

param $data

The data to cache - also depends on the concrete cache implementation

param $tags

Tags to associate with this cache entry, default: []

param $lifetime

Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL

get ( ?string $entryIdentifier)

Finds and returns data from the cache.

param $entryIdentifier

Something which identifies the cache entry - depends on concrete cache

Returns
mixed
has ( ?string $entryIdentifier)

Checks if a cache entry with the specified identifier exists.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
remove ( ?string $entryIdentifier)

Removes the given cache entry from the cache.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
flush ( )

Removes all cache entries of this cache.

flushByTag ( ?string $tag)

Removes all cache entries of this cache which are tagged by the specified tag.

param $tag

The tag the entries must have

flushByTags ( array $tags)

Removes all cache entries of this cache which are tagged by any of the specified tags.

param $tags

List of tags

collectGarbage ( )

Does garbage collection

isValidEntryIdentifier ( ?string $identifier)

Checks the validity of an entry identifier. Returns TRUE if it's valid.

param $identifier

An identifier to be checked for validity

Returns
bool
isValidTag ( ?string $tag)

Checks the validity of a tag. Returns TRUE if it's valid.

param $tag

A tag to be checked for validity

Returns
bool

The specific cache frontend implementation migth offer additional methods.

Available cache frontend implementations 

Two frontends are currently available. They primarily differ in the data types they accept and how values are handled before they are passed to the backend.

Variable frontend 

This frontend accepts strings, arrays, and objects. Values are serialized before they are written to the caching backend.

It is implemented in \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend .

PHP frontend 

This frontend is specialized for caching executable PHP files. It adds the methods requireOnce() and require() to load a cached file directly. This is useful for extensions that generate PHP code at runtime, for example when heavy reflection or dynamic class construction is involved.

It is implemented in \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend .

A backend used with the PHP frontend must implement \TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface . The file backend and the simple file backend currently fulfill this requirement.

In addition to the methods defined by \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface , it provides:

requireOnce($entryIdentifier)
Loads PHP code from the cache and require_once it right away.
require(string $entryIdentifier)
Loads PHP code from the cache and require() it right away.

Unlike require_once(), require() is safe only when the cached code can be included multiple times within a single request. Files that declare classes, functions, or constants may trigger redeclaration errors.

Cache backends 

There are a variety of different storage backends. They have different characteristics and can be used for different caching needs. The best backend depends on your server setup and hardware, as well as cache type and usage. A backend should be chosen wisely, as the wrong backend can slow down your TYPO3 installation.

Backend API 

All backends must implement the TYPO3\CMS\Core\Cache\Backend\BackendInterface.

Changed in version 14.0

BackendInterface 

interface BackendInterface
Fully qualified name
\TYPO3\CMS\Core\Cache\Backend\BackendInterface

A contract for a Cache Backend

setCache ( \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)

Sets a reference to the cache frontend which uses this backend

param $cache

The frontend for this backend

set ( ?string $entryIdentifier, ?string $data, array $tags = [], ?int $lifetime = NULL)

Saves data in the cache.

param $entryIdentifier

An identifier for this specific cache entry

param $data

The data to be stored

param $tags

Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored., default: []

param $lifetime

Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL

get ( ?string $entryIdentifier)

Loads data from the cache.

param $entryIdentifier

An identifier which describes the cache entry to load

Return description

The cache entry's content as a string or FALSE if the cache entry could not be loaded

Returns
mixed
has ( ?string $entryIdentifier)

Checks if a cache entry with the specified identifier exists.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
remove ( ?string $entryIdentifier)

Removes all cache entries matching the specified identifier.

Usually this only affects one entry but if - for what reason ever - old entries for the identifier still exist, they are removed as well.

param $entryIdentifier

Specifies the cache entry to remove

Return description

TRUE if (at least) an entry could be removed or FALSE if no entry was found

Returns
bool
flush ( )

Removes all cache entries of this cache.

collectGarbage ( )

Does garbage collection

All operations on caches must use the methods above. There are other interfaces that can be implemented by backends to add additional functionality. Extension code should not call cache backend operations directly, but should use the frontend object instead.

TaggableBackendInterface 

interface TaggableBackendInterface
Fully qualified name
\TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface

A contract for a cache backend which supports tagging.

flushByTag ( ?string $tag)

Removes all cache entries of this cache which are tagged by the specified tag.

param $tag

The tag the entries must have

flushByTags ( array $tags)

Removes all cache entries of this cache which are tagged by any of the specified tags.

param $tags

List of tags

findIdentifiersByTag ( ?string $tag)

Finds and returns all cache entry identifiers which are tagged by the specified tag

param $tag

The tag to search for

Return description

An array with identifiers of all matching entries. An empty array if no entries matched

Returns
array
setCache ( \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)

Sets a reference to the cache frontend which uses this backend

param $cache

The frontend for this backend

set ( ?string $entryIdentifier, ?string $data, array $tags = [], ?int $lifetime = NULL)

Saves data in the cache.

param $entryIdentifier

An identifier for this specific cache entry

param $data

The data to be stored

param $tags

Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored., default: []

param $lifetime

Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL

get ( ?string $entryIdentifier)

Loads data from the cache.

param $entryIdentifier

An identifier which describes the cache entry to load

Return description

The cache entry's content as a string or FALSE if the cache entry could not be loaded

Returns
mixed
has ( ?string $entryIdentifier)

Checks if a cache entry with the specified identifier exists.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
remove ( ?string $entryIdentifier)

Removes all cache entries matching the specified identifier.

Usually this only affects one entry but if - for what reason ever - old entries for the identifier still exist, they are removed as well.

param $entryIdentifier

Specifies the cache entry to remove

Return description

TRUE if (at least) an entry could be removed or FALSE if no entry was found

Returns
bool
flush ( )

Removes all cache entries of this cache.

collectGarbage ( )

Does garbage collection

PhpCapableBackendInterface 

interface PhpCapableBackendInterface
Fully qualified name
\TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface

A contract for a cache backend which is capable of storing, retrieving and including PHP source code.

requireOnce ( ?string $entryIdentifier)

Loads PHP code from the cache and require_onces it right away.

param $entryIdentifier

An identifier which describes the cache entry to load

Return description

Potential return value from the include operation

Returns
mixed
setCache ( \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)

Sets a reference to the cache frontend which uses this backend

param $cache

The frontend for this backend

set ( ?string $entryIdentifier, ?string $data, array $tags = [], ?int $lifetime = NULL)

Saves data in the cache.

param $entryIdentifier

An identifier for this specific cache entry

param $data

The data to be stored

param $tags

Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored., default: []

param $lifetime

Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL

get ( ?string $entryIdentifier)

Loads data from the cache.

param $entryIdentifier

An identifier which describes the cache entry to load

Return description

The cache entry's content as a string or FALSE if the cache entry could not be loaded

Returns
mixed
has ( ?string $entryIdentifier)

Checks if a cache entry with the specified identifier exists.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
remove ( ?string $entryIdentifier)

Removes all cache entries matching the specified identifier.

Usually this only affects one entry but if - for what reason ever - old entries for the identifier still exist, they are removed as well.

param $entryIdentifier

Specifies the cache entry to remove

Return description

TRUE if (at least) an entry could be removed or FALSE if no entry was found

Returns
bool
flush ( )

Removes all cache entries of this cache.

collectGarbage ( )

Does garbage collection

FreezableBackendInterface 

interface FreezableBackendInterface
Fully qualified name
\TYPO3\CMS\Core\Cache\Backend\FreezableBackendInterface

A contract for a cache backend which can be frozen.

freeze ( )

Freezes this cache backend.

All data in a frozen backend remains unchanged and methods which try to add or modify data result in an exception thrown. Possible expiry times of individual cache entries are ignored.

On the positive side, a frozen cache backend is much faster on read access. A frozen backend can only be thawn by calling the flush() method.

isFrozen ( )

Tells if this backend is frozen.

Returns
bool
setCache ( \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)

Sets a reference to the cache frontend which uses this backend

param $cache

The frontend for this backend

set ( ?string $entryIdentifier, ?string $data, array $tags = [], ?int $lifetime = NULL)

Saves data in the cache.

param $entryIdentifier

An identifier for this specific cache entry

param $data

The data to be stored

param $tags

Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored., default: []

param $lifetime

Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime., default: NULL

get ( ?string $entryIdentifier)

Loads data from the cache.

param $entryIdentifier

An identifier which describes the cache entry to load

Return description

The cache entry's content as a string or FALSE if the cache entry could not be loaded

Returns
mixed
has ( ?string $entryIdentifier)

Checks if a cache entry with the specified identifier exists.

param $entryIdentifier

An identifier specifying the cache entry

Return description

TRUE if such an entry exists, FALSE if not

Returns
bool
remove ( ?string $entryIdentifier)

Removes all cache entries matching the specified identifier.

Usually this only affects one entry but if - for what reason ever - old entries for the identifier still exist, they are removed as well.

param $entryIdentifier

Specifies the cache entry to remove

Return description

TRUE if (at least) an entry could be removed or FALSE if no entry was found

Returns
bool
flush ( )

Removes all cache entries of this cache.

collectGarbage ( )

Does garbage collection

Common options of caching backends 

defaultLifetime

defaultLifetime
Type
integer
Default
3600

Default lifetime in seconds of a cache entry if it has not been specified for an entry by set().

Database Backend 

This is the main backend and is suitable for most storage needs. It does not require additional server daemons or server configuration.

The database backend does not automatically perform garbage collection. Use the Scheduler garbage collection task instead.

This backend stores data in a database (usually MySQL) and can handle large amounts of data with reasonable performance. Data and tags are stored in two different tables and every cache has its own set of tables. In terms of performance, the database backend is well optimized and, if in doubt, should be used as a default backend doubt. This is the default backend if no backend is specifically set in the configuration.

The Core takes care of creating and updating database tables "on the fly".

For caches with a lot of read and write operations, it is important to tune your MySQL setup. The most important setting is innodb_buffer_pool_size. It is a good idea to give MySQL as much RAM as needed so that the main table space is completely loaded in memory.

The database backend tends to slow down if there are many write operations and big caches which don't fit into memory because of slow hard drive seek and write performance. If the data table is too big to fit into memory, this backend can compress data transparently, which shrinks the amount of space needed to 1/4 or less. The overhead of the compress/uncompress operation is usually not high. A good candidate for a cache with enabled compression is the Core pages cache: it is only read or written once per request and the data size is pretty large. Compression should not be enabled for caches which are read or written multiple times in one request.

InnoDB Issues 

The MySQL database backend uses InnoDB tables. Due to the nature of InnoDB, deleting records does not reclaim disk space. For example, if the cache uses 10GB, cleaning still keeps 10GB allocated on the disk even though phpMyAdmin shows 0 as the cache table size. To reclaim the space, turn on the MySQL option file_per_table, drop the cache tables and re-create them using the Install Tool. This does not mean that you should skip the scheduler task. Deleting records still improves performance.

Options of database backends 

compression

compression
Type
boolean
Default
false

Whether or not data should be compressed with gzip. This can reduce the size of the cache data table, but incurs CPU overhead for compression and decompression.

compressionLevel

compressionLevel
Type
integer from -1 to 9
Default
-1

Gzip compression level (if the compression option is set to true). The default compression level is usually sufficient.

-1
Default gzip compression (recommended)
0
No compression
9
Maximum compression (costs a lot of CPU)

Memcached backend 

Memcached is a simple, distributed key/value RAM database. To use this backend, at least one memcached daemon must be reachable, and the PECL module "memcache" must be loaded. There are two PHP memcached implementations: "memcache" and "memcached". Currently, only memcache is supported by this backend.

Limitations of memcached backends 

Memcached is a simple key-value store by design . Since the caching framework needs to structure it to store the identifier-data-tags relations, for each cache entry it stores an identifier->data, identifier->tags and a tag->identifiers entry.

This leads to structural problems:

  • If memcache runs out of memory but must store new entries, it will toss some other entry out of the cache (this is called an eviction in memcached speak).
  • If data is shared over multiple memcache servers and a server fails, key/value pairs on this system will just vanish from cache.

Both cases lead to corrupted caches. If, for example, a tags->identifier entry is lost, dropByTag() will not be able to find the corresponding identifier->data entries to be removed and they will not be deleted. This results in old data being delivered by the cache. There is currently no implementation of garbage collection that could rebuild cache integrity.

It is important to monitor a memcached system for evictions and server outages and to clear caches if that happens.

Furthermore, memcache has no namespacing. To distinguish entries of multiple caches from each other, every entry is prefixed with the cache name. This can lead to very long run times if a big cache needs to be flushed, as every entry has to be handled separately. It would not be possible to just truncate the whole cache with one call as this would clear the whole memcached data which might also contain non-TYPO3-related entries.

Because of the these drawbacks, the memcached backend should be used with care. It should be used in situations where cache integrity is not important or if a cache does not need to use tags. Currently, the memcache backend implements the TaggableBackendInterface, so the implementation does allow tagging, even if it is not advisable to use this backend with heavy tagging.

Options for the memcached backend 

servers

servers
Type
array
Required

true

Array of memcached servers. At least one server must be defined. Each server definition is a string, with the following valid syntaxes:

hostname or IP
TCP connect to host on memcached default port (usually 11211, defined by PHP ini variable memcache.default_port)
hostname:port
TCP connect to host on port
tcp://hostname:port
Same as above
unix:///path/to/memcached.sock
Connect to memcached server using unix sockets

compression

compression
Type
boolean
Default
false

Enable memcached internal data compression. Can be used to reduce memcached memory consumption, but adds additional compression / decompression CPU overhead on the memcached servers.

Redis Backend 

Redis is a key-value storage/database. In contrast to memcached, it allows structured values. Data is stored in RAM but it can be persisted to disk and doesn't suffer from the design problems of the memcached backend implementation. The redis backend can be used as an alternative to the database backend for big cache tables and help to reduce load on database servers this way. The implementation can handle millions of cache entries, each with hundreds of tags if the underlying server has enough memory.

Redis is extremely fast but very memory hungry. The implementation is an option for big caches with lots of data because most operations perform O(1) in proportion to the number of (redis) keys. This basically means that access to an entry in a cache with a million entries takes the same time as to a cache with only 10 entries, as long as there is enough memory available to hold the complete set in memory. At the moment only one redis server can be used at a time per cache, but one redis instance can handle multiple caches without performance loss when flushing a single cache.

The implementation is based on the PHP phpredis module, which must be available on the system.

Redis example 

The Redis caching backend configuration is very similar to that of other backends, with one caveat.

TYPO3 caches should be separated if the same keys are used. This applies to the pages and pagesection caches. Both use "tagIdents:pageId_21566" for a page with id 21566. How you separate them is for a system administrator to decide. We provide examples with several databases but this may not be the best option in production where you might want to use multiple cores (which do not support databases). Separation is also a good idea because caches can be flushed individually.

If you have several of your own caches which each use unique keys (for example by using a different prefix for each separate cache identifier), you can store them in the same database, but it is good practice to separate the core caches.

In practical terms, Redis databases should be used to separate different keys belonging to the same application (if needed), and not to use a single Redis instance for multiple unrelated applications.

https://redis.io/commands/select/

config/system/additional.php | typo3conf/system/additional.php
<?php

$redisHost = '127.0.0.1';
$redisPort = 6379;
$redisCaches = [
    'pages' => [
        'defaultLifetime' => 86400 * 7, // 1 week
        'compression' => true,
    ],
    'pagesection' => [
        'defaultLifetime' => 86400 * 7,
    ],
    'hash' => [],
    'rootline' => [],
];

$redisDatabase = 0;
foreach ($redisCaches as $name => $values) {
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$name]['backend']
        = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$name]['options'] = [
        'database' => $redisDatabase++,
        'hostname' => $redisHost,
        'port' => $redisPort,
    ];
    if (isset($values['defaultLifetime'])) {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$name]['options']['defaultLifetime']
            = $values['defaultLifetime'];
    }
    if (isset($values['compression'])) {
        $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$name]['options']['compression']
            = $values['compression'];
    }
}
Copied!

Options for the redis caching backend 

servers

servers
Type
string
Default
127.0.0.1

IP address or name of redis server to connect to.

port

port
Type
integer
Default
6379

Port of the redis daemon.

persistentConnection

persistentConnection
Type
boolean
Default
false

Activate a persistent connection to a redis server. This is a good idea in high load cloud setups.

database

database
Type
integer
Default
0

Number of the database to store entries. Each cache should have its own database, otherwise caches sharing a database are all flushed if the flush operation is issued to one of them. Database numbers 0 and 1 are used and flushed by the Core unit tests and should not be used if possible.

password

password
Type
string

Password used to connect to the redis instance if the redis server needs authentication.

compression

compression
Type
boolean
Default
false

Whether or not data compression with gzip should be enabled. This can reduce cache size, but adds some CPU overhead for the compression and decompression operations in PHP.

compressionLevel

compressionLevel
Type
integer from -1 to 9
Default
-1

Set gzip compression level to a specific value. The default compression level is usually sufficient.

-1
Default gzip compression (recommended)
0
No compression
9
Maximum compression (but more CPU overhead)

Redis server configuration 

This section is about the configuration on the Redis server, not the client.

For flushing by cache tags to work, it is important that the integrity of the cache entries and cache tags is maintained. This may not be the case, depending on which eviction policy (maxmemory-policy) is used. For example, for a page id=81712, the following entries may exist in the Redis page cache:

  1. tagIdents:pageId_81712 (tag->identifier relation)
  2. identTags:81712_7e9c8309692aa221b08e6d5f6ec09fb6 (identifier->tags relation)
  3. identData:81712_7e9c8309692aa221b08e6d5f6ec09fb6 (identifier->data)

If entries are evicted (due to memory shortage), there is no mechanism which ensures that all related entries will be evicted. If maxmemory-policy allkeys-lru is used, for example, this may result in the situation that the cache entry (identData) still exists, but the tag entry (tagIdents) does not. The tag entry reflects the relation "cache tag => cache identifier" and is used for RedisBackend::flushByTag()). If this entry is gone, the cache can no longer be flushed if content is changed on the page or an explicit flushing of the page cache for this page is requested. Once this is the case, cache flushing (for this page) is only possible via other means (such as full cache flush).

Because of this, the following recommendations apply:

  1. Allocate enough memory (maxmemory) for the cache.
  2. Use the maxmemory-policy volatile-ttl. This will ensure that no tagIdents entries are removed. (These have no expiration date).
  3. Regularly run the TYPO3 scheduler garbage collection task for the Redis cache backend.
  4. Monitor evicted_keys in case an eviction policy is used.
  5. Monitor used_memory if eviction policy noeviction is used. The used_memory should always be less then maxmemory.

The Eviction policy options have the following drawbacks:

volatile-ttl
(recommended) Will flush only entries with an expiration date. Should be ok with TYPO3.
noeviction
(Not recommended) Once memory is full, no new entries will be saved to cache. Only use if you can ensure that there is always enough memory.
allkeys-lru, allkeys-lfu, allkeys-random
(Not recommended) This may result in tagIdents being removed, but not the related identData entry, which makes it impossible to flush the cache entries by tag (which is necessary for TYPO3 cache flushing on changes to work and the flush page cache to work for specific pages).

File backend 

The file backend stores every cache entry as a single file in the file system. The lifetime and tags are added to the file after the data section.

This backend is the big brother of the Simple file backend and implements additional interfaces. Like the simple file backend it also implements the PhpCapableInterface, so it can be used with PhpFrontend. In contrast to the simple file backend it furthermore implements TaggableInterface and FreezableInterface.

A frozen cache does no lifetime check and has a list of all existing cache entries that is reconstituted during initialization. As a result, a frozen cache needs less file system look ups and calculation time if accessing cache entries. On the other hand, a frozen cache can not manipulate (remove, set) cache entries anymore. A frozen cache must flush the complete cache again to make cache entries writable again. Freezing caches is currently not used in the TYPO3 Core. It can be an option for code logic that is able to calculate and set all possible cache entries during some initialization phase, to then freeze the cache and use those entries until the whole thing is flushed again. This can be useful especially if caching PHP code.

In general, the backend was specifically optimized to cache PHP code because the get and set operations have low overhead. The file backend is not very good at tagging and does not scale well with the number of tags. Do not use this backend if cached data has many tags.

Options for the file backend 

cacheDirectory

cacheDirectory
Type
array
Default
var/cache/

The directory where the cache files are stored. By default, it is assumed that the directory is below TYPO3_DOCUMENT_ROOT. However, an absolute path could be selected. Every cache should be assigned its own directory, otherwise flushing of one cache would flush all other caches in the same directory.

Simple File Backend 

The simple file backend is the small brother of the file backend. In contrast to most other backends, it does not implement the TaggableInterface, so cache entries cannot be tagged and flushed by tag. This improves performance if cache entries do not need such tagging. The TYPO3 Core uses this backend for its central Core cache (it holds autoloader cache entries and other important cache entries). The Core cache is usually flushed completely and does not need specific cache entry eviction.

PDO Backend 

The PDO backend can be used as a native PDO interface to databases which are connected to PHP via PDO. It is an alternative to the database backend if a cache should be stored in a database which is otherwise only supported by TYPO3 dbal to reduce the parser overhead.

Garbage collection is implemented for this backend and should be called to clean up hard disk space or memory.

Options for the PDO backend 

dataSourceName

dataSourceName
Type
string
Required

true

Data source name for connecting to the database. Examples:

  • mysql:host=localhost;dbname=test
  • sqlite:/path/to/sqlite.db
  • sqlite::memory

username

username
Type
string

Username for the database connection.

password

password
Type
string

Password to use for the database connection.

Transient Memory Backend 

The transient memory backend stores data in a PHP array. It is only valid for one request. This is useful if code logic carries out expensive calculations or repeatedly looks up identical information in a database. Data is stored once in an array and data entries are retrieved from the cache in consecutive calls, getting rid of additional overhead. Since caches are available system-wide and shared between Core and extensions, they can share the same information.

Since the data is stored directly in memory, this backend is the quickest. The stored data adds to the memory consumed by the PHP process and can hit the memory_limit PHP setting.

Null Backend 

The null backend is a dummy backend which doesn't store any data and always returns false on get(). This backend is useful in a development context to "switch off" a cache.

Developer information 

This chapter is aimed at extension authors who want to use the caching framework for their needs. It is about how to use the framework properly. For details about its inner working, please refer to the section about architecture.

Example usages can be found throughout the TYPO3 Core, in particular in the system extensions core and extbase.

Cache registration 

Registration of a new cache should be done in an extension's ext_localconf.php. The example below defines an empty sub-array in cacheConfigurations. Neither frontend nor backend are defined: The cache manager will choose the default variable frontend and the database backend by default.

EXT:my_extension/ext_localconf.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myext_mycache']
    ??= [];
Copied!

If special settings are needed, for example, a specific backend (like the transient memory backend), it can be defined with an additional line below the cache array declaration. The extension documentation should hint an integrator about specific caching needs or setups in this case.

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myext_mycache']
    ??= [];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myext_mycache']['backend']
    ??= TransientMemoryBackend::class;
Copied!

Using the cache 

First, we need to prepare the injection of our cache by setting it up as service in the container service configuration:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  cache.myext_mycache:
    class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
    factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
    arguments: ['myext_mycache']
Copied!

Read how to configure dependency injection in extensions.

The name of the service for the injection configuration is cache.myext_mycache, the name of the cache is myext_mycache (as defined in ext_localconf.php). Both can be anything you like, just make sure they are unique and clearly hint at the purpose of your cache.

Here is some example code which retrieves the cache via dependency injection:

EXT:my_extension/Classes/MyClass.php
<?php

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

final class MyClass
{
    public function __construct(
        private readonly FrontendInterface $cache,
    ) {}

    //...

    private function getCachedValue(string $cacheIdentifier, array $tags, int|null $lifetime): array
    {
        // If value is false, it has not been cached
        $value = $this->cache->get($cacheIdentifier);
        if ($value === false) {
            // Store the data in cache
            $value = $this->calculateData();
            $this->cache->set($cacheIdentifier, $value, $tags, $lifetime);
        }

        return $value;
    }

    private function calculateData(): array
    {
        $data = [];
        // todo: implement
        return $data;
    }
}
Copied!

Since the auto-wiring feature of the dependency injection container cannot detect which cache configuration should be used for the $cache argument of MyClass, the container service configuration needs to be extended:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration
  # and the configuration of the cache from above

  MyVendor\MyExtension\MyClass:
    arguments:
      $cache: '@cache.myext_mycache'
Copied!

Read how to configure dependency injection in extensions.

Here @cache.myext_mycache refers to the cache service we defined above. This setup allows you to freely inject the very same cache into any class.

Working with cache tags 

The frontend cache collector API is available as a PSR-7 request attribute to collect cache tags and their corresponding lifetime. Find more information in the chapter Frontend cache collector.

System categories 

TYPO3 provides a generic categorization system. Categories can be created in the backend like any other type of record.

A TCA field of the column type category is available.

Pages, content elements and files contain category fields by default.

Using Categories 

Managing Categories 

System categories are defined like any other record. Each category can have a parent, making a tree-like structure.

A category with a parent defined

The Items tab shows all related records, for example all records that have been marked as belonging to this category.

Adding categories to a table 

Categories can be added to a table by defining a TCA field of the TCA column type category. While using this type, TYPO3 takes care of generating the necessary TCA configuration and also adds the database column automatically. Developers only have to configure the TCA column and add it to the desired record types:

$GLOBALS['TCA'][$myTable]['columns']['categories'] = [
   'config' => [
      'type' => 'category'
   ]
];

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
   $myTable,
   'categories'
);
Copied!

This is the result of the above code:

The newly added field to define relations to categories

Using categories in FlexForms 

It is possible to create relations to categories also in FlexForms.

Due to some limitations in FlexForm, the property relationship manyToMany is not supported. Therefore, the default value for this property is oneToMany.

<T3DataStructure>
    <ROOT>
        <TCEforms>
            <sheetTitle>aTitle</sheetTitle>
        </TCEforms>
        <type>array</type>
        <el>
            <categories>
                <TCEforms>
                    <config>
                        <type>category</type>
                    </config>
                </TCEforms>
            </categories>
        </el>
    </ROOT>
</T3DataStructure>
Copied!

System categories API 

Category Collections 

The \TYPO3\CMS\Core\Category\Collection\CategoryCollection class provides the API for retrieving records related to a given category. It is extended by class \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection which does the same job but in the frontend, i.e. respecting all enable fields and performing version and language overlays.

The main method is load() which will return a traversable list of items related to the given category. Here is an example usage, taken from the RECORDS content object:

$collection = \TYPO3\CMS\Frontend\Category\Collection\CategoryCollection::load(
   $aCategory,
   true,
   $table,
   $relationField
);
if ($collection->count() > 0) {
   // Add items to the collection of records for the current table
   foreach ($collection as $item) {
      $tableRecords[$item['uid']] = $item;
      // Keep track of all categories a given item belongs to
      if (!isset($categoriesPerRecord[$item['uid']])) {
         $categoriesPerRecord[$item['uid']] = [];
      }
      $categoriesPerRecord[$item['uid']][] = $aCategory;
   }
}
Copied!

As all collection classes in the TYPO3 Core implement the Iterator interface, it is also possible to use expected methods like next(), rewind(), etc. Note that methods such as add() will only add items to the collection temporarily. The relations are not persisted in the database.

Usage with TypoScript 

In the frontend, it is possible to get collections of categorized records loaded into a RECORDS content object for rendering. Check out the categories property.

The HMENU object also has a "categories" special type to display a menu based on categorized pages.

User permissions for system categories 

In most aspects, system categories are treated like any other record. They can be viewed or edited by editors if they are stored in a folder where the editor has access to and if the table sys_category is allowed in the field Tables (listing) and Tables (modify) in the tab Access Lists of the user group.

Additionally it is possible to set Mounts and Workspaces > Category Mounts in the user group. If at least one category is set in the category mounts only the chosen categories are allowed to be attached to records.

Code editor 

Changed in version 13.0

The code editor functionality was moved from the optional "t3editor" system extension into the "backend" system extension. The code editor is therefore always available.

Checks whether the previous system extension is installed via \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('t3editor') are now obsolete.

The code editor provides a backend editor with syntax highlighting. The editor is used by TYPO3 itself for TCA fields with type "text" and renderType "codeEditor" and in the module File > Filelist. Under the hood, CodeMirror is used.

Usage in TCA 

Extensions may configure backend fields to use the code editor by TCA. The editor is only available for fields of type text. By setting the renderType to codeEditor the syntax highlighting can be activated.

By setting the property format the mode for syntax highlighting can be chosen. Allowed values:

  • css
  • html
  • javascript
  • json
  • php
  • sql
  • typoscript
  • xml
  • and any custom mode registered by an extension.

Example 

Excerpt of TCA definition (EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_mytable.php)
[
    'columns' => [
        'codeeditor1' => [
            'label' => 'codeEditor_1 format=html, rows=7',
            'description' => 'field description',
            'config' => [
                'type' => 'text',
                'renderType' => 'codeEditor',
                'format' => 'html',
                'rows' => 7,
            ],
        ],
    ],
]
Copied!

Displays an editor like the following:

A code editor with syntax highlighting for HTML

A code editor with syntax highlighting for HTML

Extend the code editor 

Custom modes (used for syntax highlighting) and addons can be registered.

CodeMirror delivers a lot more modes and addons than registered in the code editor by default.

More supported addons and modes are available at:

To register custom addons and modes, extensions may have these two files to extend the code editor:

  • Configuration/Backend/T3editor/Addons.php
  • Configuration/Backend/T3editor/Modes.php

Both files return an array, as known as in TCA and backend routes, for example.

Register an addon 

To register an addon, the following code may be used:

EXT:my_extension/Configuration/Backend/T3editor/Addons.php
<?php

use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

return [
    'my/addon' => [
        'module' => JavaScriptModuleInstruction::create(
            '@codemirror/addon',
            'addon',
        )->invoke(),
        'cssFiles' => [
            'EXT:my_extension/Resources/Public/Css/MyAddon.css',
        ],
        'options' => ['foobar' => 'baz'],
    ],
    'modes' => ['htmlmixed', 'xml'],
];
Copied!

The following configuration options are available:

<identifier>

<identifier>
Type
string
Required
true

Represents the unique identifier of the module ( my/addon in this example).

module

module
Type
string
Required
true

Holds the JavaScriptModuleInstruction of the CodeMirror module.

cssFiles

cssFiles
Type
array

Holds all CSS files that must be loaded for the module.

options

options
Type
array

Options that are used by the addon.

modes

modes
Type
array

If set, the addon is only loaded if any of the modes supplied here is used.

Register a mode 

To register a mode, the following code may be used:

EXT:my_extension/Configuration/Backend/T3editor/Modes.php
<?php

return [
    'css' => [
        'module' => 'cm/mode/css/css',
        'extensions' => ['css'],
    ],
];
Copied!

The following configuration options are available:

<identifier>

<identifier>
Type
string
Required
true

Represents the unique identifier and format code of the mode (css in this example). The format code is used in TCA to define the CodeMirror mode to be used.

Example:

$GLOBALS['TCA']['tt_content']['types']['css']['columnsOverrides']['bodytext']['config']['format'] = 'css';
Copied!

module

module
Type
string
Required
true

Holds the JavaScriptModuleInstruction of the CodeMirror module.

extensions

extensions
Type
array

Binds the mode to specific file extensions. This is important for using the code editor in the module File > Filelist.

default

default
Type
bool

If set, the mode is used as fallback if no sufficient mode is available. By factory default, the default mode is html.

Console commands (CLI) 

TYPO3 supports running scripts from the command line. This functionality is especially useful for automating tasks such as cache clearing, maintenance, and scheduling jobs.

System administrators and developers can use predefined commands to interact with the TYPO3 installation directly from the terminal.

To learn how to run TYPO3 CLI commands in various environments (such as Composer projects, Classic installations, DDEV, Docker, or remote servers), refer to How to run a command.

A list of available Core commands is provided here: List of Core console commands. Additional commands may be available depending on installed extensions. The extension helhum/typo3-console is frequently installed to provide a wider range of CLI commands.

If you are developing your own TYPO3 extension, you can also create custom console commands to provide functionality specific to your use case. These commands integrate with the TYPO3 CLI and can be executed like any built-in command.

For more information, see Writing Custom Commands.

Command usage in terminal environments 

To execute TYPO3 console commands, you need access to a terminal (command line interface). You can run commands locally or on a remote server.

The entry point for running commands depends on the type of TYPO3 installation.

To display a list of all available commands, use the following:

vendor/bin/typo3
Copied!
typo3/sysext/core/bin/typo3
Copied!
ddev typo3
Copied!
bin/typo3
Copied!

Local development environments 

If you are working on a local development environment such as DDEV, MAMP, or a native PHP installation, open a terminal and navigate to your project directory. Then, run the command using the appropriate entry point for your installation (Composer or Classic mode).

cd my-typo3-project
vendor/bin/typo3 list
Copied!

Using the CLI with DDEV 

If you are using DDEV, you can run TYPO3 commands from your host machine using the ddev typo3 shortcut. This automatically routes the command into the correct container and environment.

For example, to flush all caches:

ddev typo3 cache:flush
Copied!

You can use this shortcut with any TYPO3 CLI command:

ddev typo3 <your-command>
Copied!

Using the CLI in Docker containers 

If you are using Docker directly (without DDEV or a wrapper), TYPO3 commands must usually be executed inside the container that runs PHP and TYPO3.

First, open a shell inside the container. For example:

docker exec -it my_php_container bash
Copied!

Replace my_php_container with the name of your running PHP container.

Once inside the container, navigate to the project directory and run the command:

cd /var/www/html
vendor/bin/typo3 list
Copied!

You typically cannot run TYPO3 commands from the host system unless the project's PHP environment is directly accessible outside the container.

Executing commands on remote servers via SSH 

For TYPO3 installations on a remote server, you typically access the server using SSH (Secure Shell).

Use the following command to connect:

ssh username@example.com
Copied!

Replace username with your SSH username and example.com with the server hostname or IP address.

Once logged in, navigate to your TYPO3 project directory and execute the command. For example:

cd /var/www/my-typo3-site
vendor/bin/typo3 list
Copied!

Making the CLI entry point executable 

The TYPO3 command entry point (e.g. vendor/bin/typo3) must be marked as executable in order to run it directly.

To make the file executable, run:

chmod +x vendor/bin/typo3
Copied!

If you do not have permission to change the file mode or the file system is read-only, you can run the script by calling the PHP interpreter explicitly:

php vendor/bin/typo3 list
Copied!

This method works even if the file is not executable.

Executing commands from the scheduler 

By default, it is possible to run a command from the TYPO3 Scheduler as well. To do this, select the task Execute console commands followed by your command in the Schedulable Command field.

In order to prevent commands from being set up as scheduler tasks, see Making a command non-schedulable.

Using CLI commands in CI/CD pipelines 

In continuous integration (CI) or continuous deployment (CD) pipelines, TYPO3 CLI commands can be used for tasks such as preparing the system, checking configuration, or activating extensions (Classic mode).

See also chapter CI/CD: Automatic deployment for TYPO3 Projects.

The exact command entry point depends on your installation type. For example:

vendor/bin/typo3 list
Copied!

Before you can run most TYPO3 CLI commands, ensure the following:

  • You have run composer install so that the vendor/ directory and CLI entry point exist.
  • The PHP environment is set up (with extensions such as pdo_mysql).
  • The environment variable TYPO3_CONTEXT is set appropriately, such as Production or Testing.

If you want to run commands such as cache:flush after deployment, it is common to use the CI pipeline to connect to the remote server and execute the command there using SSH:

ssh user@your-server.example.com 'cd /var/www/html && vendor/bin/typo3 cache:flush'
Copied!

This pattern is often used in post-deployment steps to finalize setup in the live environment.

Clearing the cache with cache:flush 

A common use case is to use a console command to flush all caches, for example during development.

vendor/bin/typo3 cache:flush
Copied!
typo3/sysext/core/bin/typo3 cache:flush
Copied!

Viewing help for CLI commands 

Show help for the command:

vendor/bin/typo3 cache:flush -h
Copied!
typo3/sysext/core/bin/typo3 cache:flush -h
Copied!

List of TYPO3 console commands 

By default TYPO3 ships the listed console commands, depending on which system extensions are installed.

Third party extensions can define custom console commands.

The extension helhum/typo3-console ships many commands to execute TYPO3 actions, which otherwise would only be accessible via the TYPO3 backend.

This page assumes that the code is run on a Composer based installation with default binaries location. Here you can read how to run them in general and on Classic mode installations: Run a command from the command line.

List all TYPO3 console commands 

Command Description
global
 
vendor/bin/typo3 completion
Dump the shell completion script
vendor/bin/typo3 help
Display help for a command
vendor/bin/typo3 list
List commands
vendor/bin/typo3 setup
Setup TYPO3 via CLI using environment variables, CLI options or interactive
backend
 
vendor/bin/typo3 backend:lock
Lock the TYPO3 Backend
vendor/bin/typo3 backend:resetpassword
Trigger a password reset for a backend user
vendor/bin/typo3 backend:unlock
Unlock the TYPO3 Backend
vendor/bin/typo3 backend:user:create
Create a backend user
cache
 
vendor/bin/typo3 cache:flush
Flush TYPO3 caches.
vendor/bin/typo3 cache:warmup
Warmup TYPO3 caches.
cleanup
 
vendor/bin/typo3 cleanup:deletedrecords
Permanently deletes all records marked as "deleted" in the database.
vendor/bin/typo3 cleanup:flexforms
Clean up database FlexForm fields that do not match the chosen data structure.
vendor/bin/typo3 cleanup:localprocessedfiles
Delete processed files and their database records.
vendor/bin/typo3 cleanup:missingrelations
Find all record references pointing to a non-existing record
vendor/bin/typo3 cleanup:orphanrecords
Find and delete records that have lost their connection with the page tree
vendor/bin/typo3 cleanup:previewlinks
Find all versioned records and possibly cleans up invalid records in the database.
vendor/bin/typo3 cleanup:versions
Find all versioned records and possibly cleans up invalid records in the database.
extension
 
vendor/bin/typo3 extension:list
Shows the list of extensions available to the system
vendor/bin/typo3 extension:setup
Set up extensions
fluid
 
vendor/bin/typo3 fluid:schema:generate
Generate XSD schema files for all available ViewHelpers in var/transient/
impexp
 
vendor/bin/typo3 impexp:export
Exports a T3D / XML file with content of a page tree
vendor/bin/typo3 impexp:import
Imports a T3D / XML file with content into a page tree
language
 
vendor/bin/typo3 language:update
Update the language files of all activated extensions
lint
 
vendor/bin/typo3 lint:yaml
Lint a YAML file and outputs encountered errors
mailer
 
vendor/bin/typo3 mailer:spool:send
Sends emails from the spool
messenger
 
vendor/bin/typo3 messenger:consume
Consume messages
redirects
 
vendor/bin/typo3 redirects:checkintegrity
Check integrity of redirects
vendor/bin/typo3 redirects:cleanup
Cleanup old redirects periodically for given constraints like days, hit count or domains.
referenceindex
 
vendor/bin/typo3 referenceindex:update
Update the reference index of TYPO3
scheduler
 
vendor/bin/typo3 scheduler:execute
Execute given Scheduler tasks.
vendor/bin/typo3 scheduler:list
List all Scheduler tasks.
vendor/bin/typo3 scheduler:run
Start the TYPO3 Scheduler from the command line.
setup
 
vendor/bin/typo3 setup:begroups:default
Setup default backend user groups
site
 
vendor/bin/typo3 site:list
Shows the list of sites available to the system
vendor/bin/typo3 site:sets:list
Shows the list of available site sets
vendor/bin/typo3 site:show
Shows the configuration of the specified site
syslog
 
vendor/bin/typo3 syslog:list
Show entries from the sys_log database table of the last 24 hours.
upgrade
 
vendor/bin/typo3 upgrade:list
List available upgrade wizards.
vendor/bin/typo3 upgrade:mark:undone
Mark upgrade wizard as undone.
vendor/bin/typo3 upgrade:run
Run upgrade wizard. Without arguments all available wizards will be run.
workspace
 
vendor/bin/typo3 workspace:autopublish
Publish a workspace with a publication date.

vendor/bin/typo3 completion

vendor/bin/typo3 completion Back to list
Dump the shell completion script
Usage
vendor/bin/typo3 completion [--debug] [--] [<shell>]
Copied!
Arguments

shell

shell
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
Options

--debug

--debug
Tail the completion debug log
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The completion command dumps the shell completion script required to use shell autocompletion (currently, bash, fish, zsh completion are supported).

Static installation

Dump the script to a global completion file and restart your shell:

vendor/vendor/bin/typo3 completion  | sudo tee /etc/bash_completion.d/typo3
Copied!

Or dump the script to a local file and source it:

vendor/vendor/bin/typo3 completion  > completion.sh

# source the file whenever you use the project
source completion.sh

# or add this line at the end of your "~/.bashrc" file:
source /path/to/completion.sh
Copied!
Dynamic installation

Add this to the end of your shell configuration file (e.g. "~/.bashrc"):

eval "$(/var/www/html/vendor/vendor/bin/typo3 completion )"
Copied!

vendor/bin/typo3 help

vendor/bin/typo3 help Back to list
Display help for a command
Usage
vendor/bin/typo3 help [--format FORMAT] [--raw] [--] [<command_name>]
Copied!
Arguments

command_name

command_name
The command name
Options

--format

--format
The output format (txt, xml, json, or md)
Value
Required
Default value
"txt"

--raw

--raw
To output raw command help
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The help command displays help for a given command:

vendor/vendor/bin/typo3 help list
Copied!

You can also output the help in other formats by using the --format option:

vendor/vendor/bin/typo3 help --format=xml list
Copied!

To display the list of available commands, please use the list command.

vendor/bin/typo3 list

vendor/bin/typo3 list Back to list
List commands
Usage
vendor/bin/typo3 list [--raw] [--format FORMAT] [--short] [--] [<namespace>]
Copied!
Arguments

namespace

namespace
The namespace name
Options

--raw

--raw
To output raw command list
Value
None allowed
Default value
false

--format

--format
The output format (txt, xml, json, or md)
Value
Required
Default value
"txt"

--short

--short
To skip describing commands' arguments
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The list command lists all commands:

vendor/vendor/bin/typo3 list
Copied!

You can also display the commands for a specific namespace:

vendor/vendor/bin/typo3 list test
Copied!

You can also output the information in other formats by using the --format option:

vendor/vendor/bin/typo3 list --format=xml
Copied!

It's also possible to get raw list of commands (useful for embedding command runner):

vendor/vendor/bin/typo3 list --raw
Copied!

vendor/bin/typo3 setup

vendor/bin/typo3 setup Back to list
Setup TYPO3 via CLI using environment variables, CLI options or interactive
Usage
vendor/bin/typo3 setup [--driver [DRIVER]] [--host HOST] [--port [PORT]] [--dbname DBNAME] [--username USERNAME] [--password PASSWORD] [--admin-username [ADMIN-USERNAME]] [--admin-user-password ADMIN-USER-PASSWORD] [--admin-email ADMIN-EMAIL] [--project-name PROJECT-NAME] [--create-site [CREATE-SITE]] [--server-type [SERVER-TYPE]] [--force] [-n|--no-interaction]
Copied!
Options

--driver

--driver
Select which database driver to use
Value
Optional

--host

--host
Set the database host to use
Value
Required
Default value
"db"

--port

--port
Set the database port to use
Value
Optional
Default value
"3306"

--dbname

--dbname
Set the database name to use
Value
Required
Default value
"db"

--username

--username
Set the database username to use
Value
Required
Default value
"db"

--password

--password
Set the database password to use
Value
Required

--admin-username

--admin-username
Set a username
Value
Optional
Default value
"admin"

--admin-user-password

--admin-user-password
Set users password
Value
Required

--admin-email

--admin-email
Set users email
Value
Required
Default value
""

--project-name

--project-name
Set the TYPO3 project name
Value
Required
Default value
"New TYPO3 Project"

--create-site

--create-site
Create a basic site setup (root page and site configuration) with the given domain
Value
Optional
Default value
false

--server-type

--server-type
Define the web server the TYPO3 installation will be running on
Value
Optional
Default value
"other"

--force

--force
Force settings overwrite - use this if TYPO3 has been installed already
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help
The command offers 3 ways to setup TYPO3:
  1. environment variables
  2. commandline options
  3. interactive guided walk-through

All values are validated no matter where it was set. If a value is missing, the user will be asked for it.

Setup using environment variables

TYPO3_DB_DRIVER=mysqli \
TYPO3_DB_USERNAME=db \
TYPO3_DB_PORT=3306 \
TYPO3_DB_HOST=db \
TYPO3_DB_DBNAME=db \
TYPO3_SETUP_ADMIN_EMAIL=admin@example.com \
TYPO3_SETUP_ADMIN_USERNAME=admin \
TYPO3_SETUP_CREATE_SITE="https://your-typo3-site.com/" \
TYPO3_PROJECT_NAME="Automated Setup" \
TYPO3_SERVER_TYPE="apache" \
./vendor/bin/typo3 setup --force

Copied!

vendor/bin/typo3 backend:lock

vendor/bin/typo3 backend:lock Back to list
Lock the TYPO3 Backend
Usage
vendor/bin/typo3 backend:lock [<redirect>]
Copied!
Arguments

redirect

redirect
If set, a locked TYPO3 Backend will redirect to URI specified with this argument. The URI is saved as a string in the lockfile that is specified in the system configuration.
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Lock the TYPO3 Backend

vendor/bin/typo3 backend:resetpassword

vendor/bin/typo3 backend:resetpassword Back to list
Trigger a password reset for a backend user
Usage
vendor/bin/typo3 backend:resetpassword <backendurl> <email>
Copied!
Arguments

backendurl

backendurl
The URL of the TYPO3 Backend, e.g. https://www.example.com/typo3/

email

email
The email address of a valid backend user
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Trigger a password reset for a backend user

vendor/bin/typo3 backend:unlock

vendor/bin/typo3 backend:unlock Back to list
Unlock the TYPO3 Backend
Usage
vendor/bin/typo3 backend:unlock
Copied!
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Unlock the TYPO3 Backend

vendor/bin/typo3 backend:user:create

vendor/bin/typo3 backend:user:create Back to list
Create a backend user
Usage
vendor/bin/typo3 backend:user:create [-u|--username USERNAME] [-p|--password PASSWORD] [-e|--email EMAIL] [-g|--groups GROUPS] [-a|--admin] [-m|--maintainer]
Copied!
Options

--username

--username / -u
The username of the backend user
Value
Required

--password

--password / -p
The password of the backend user. See security note below.
Value
Required

--email

--email / -e
The email address of the backend user
Value
Required
Default value
""

--groups

--groups / -g
Assign given groups to the user
Value
Required

--admin

--admin / -a
Create user with admin privileges
Value
None allowed
Default value
false

--maintainer

--maintainer / -m
Create user with maintainer privileges
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Create a backend user using environment variables

Example:

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_ADMIN=0 \
TYPO3_BE_USER_MAINTAINER=0 \
./vendor/bin/typo3 backend:user:create --no-interaction
Copied!

vendor/bin/typo3 cache:flush

vendor/bin/typo3 cache:flush Back to list
Flush TYPO3 caches.
Usage
vendor/bin/typo3 cache:flush [-g|--group [GROUP]]
Copied!
Options

--group

--group / -g
The cache group to flush (system, pages, di or all)
Value
Optional
Default value
"all"

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

This command can be used to clear the caches, for example after code updates in local development and after deployments.

vendor/bin/typo3 cache:warmup

vendor/bin/typo3 cache:warmup Back to list
Warmup TYPO3 caches.
Usage
vendor/bin/typo3 cache:warmup [-g|--group [GROUP]]
Copied!
Options

--group

--group / -g
The cache group to warmup (system, pages, di or all)
Value
Optional
Default value
"all"

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

This command is useful for deployments to warmup caches during release preparation.

vendor/bin/typo3 cleanup:deletedrecords

vendor/bin/typo3 cleanup:deletedrecords Back to list
Permanently deletes all records marked as "deleted" in the database.
Usage
vendor/bin/typo3 cleanup:deletedrecords [-p|--pid PID] [-d|--depth DEPTH] [--dry-run] [-m|--min-age MIN-AGE]
Copied!
Options

--pid

--pid / -p
Setting start page in page tree. Default is the page tree root, 0 (zero)
Value
Required

--depth

--depth / -d
Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.
Value
Required

--dry-run

--dry-run
If this option is set, the records will not actually be deleted, but just the output which records would be deleted are shown
Value
None allowed
Default value
false

--min-age

--min-age / -m
Minimum age in days records need to be marked for deletion before actual deletion
Value
Required
Default value
0

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Traverse page tree and find and flush deleted records. If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 cleanup:flexforms

vendor/bin/typo3 cleanup:flexforms Back to list
Clean up database FlexForm fields that do not match the chosen data structure.
Usage
vendor/bin/typo3 cleanup:flexforms [--dry-run]
Copied!
Options

--dry-run

--dry-run
If this option is set, the records will not be updated, but only show the output which records would have been updated.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Clean up records with dirty FlexForm values not reflected in current data structure.

vendor/bin/typo3 cleanup:localprocessedfiles

vendor/bin/typo3 cleanup:localprocessedfiles Back to list
Delete processed files and their database records.
Usage
vendor/bin/typo3 cleanup:localprocessedfiles [--dry-run] [--all] [-f|--force]
Copied!
Options

--dry-run

--dry-run
If set, the records and files which would be deleted are displayed.
Value
None allowed
Default value
false

--all

--all
If set, ALL processed-file (driver=Local) records will be removed, also those without identifier ("stubs" for unprocessed files) and existing files.
Value
None allowed
Default value
false

--force

--force / -f
Force cleanup. When set the confirmation question will be skipped. When using --no-interaction, --force will be set automatically.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 cleanup:missingrelations

vendor/bin/typo3 cleanup:missingrelations Back to list
Find all record references pointing to a non-existing record
Usage
vendor/bin/typo3 cleanup:missingrelations [--dry-run] [--update-refindex]
Copied!
Options

--dry-run

--dry-run
If this option is set, the references will not be removed, but just the output which references would be deleted are shown
Value
None allowed
Default value
false

--update-refindex

--update-refindex
Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Assumptions: - a perfect integrity of the reference index table (always update the reference index table before using this tool!) - all database references to check are integers greater than zero - does not check if a referenced record is inside an offline branch, another workspace etc. which could make the reference useless in reality or otherwise question integrity Records may be missing for these reasons (except software bugs): - someone deleted the record which is technically not an error although it might be a mistake that someone did so. - after flushing published versions and/or deleted-flagged records a number of new missing references might appear; those were pointing to records just flushed.

An automatic repair is only possible for managed references are (not for soft references), for offline versions records and non-existing records. If you just want to list them, use the --dry-run option. The references in this case are removed.

If the option "--dry-run" is not set, all managed files (TCA/FlexForm attachments) will silently remove the references to non-existing and offline version records. All soft references with relations to non-existing records, offline versions and deleted records require manual fix if you consider it an error.

Manual repair suggestions: - For soft references you should investigate each case and edit the content accordingly. - References to deleted records can theoretically be removed since a deleted record cannot be selected and hence your website should not be affected by removal of the reference. On the other hand it does not hurt to ignore it for now. To have this automatically fixed you must first flush the deleted records after which remaining references will appear as pointing to Non Existing Records and can now be removed with the automatic fix.

If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 cleanup:orphanrecords

vendor/bin/typo3 cleanup:orphanrecords Back to list
Find and delete records that have lost their connection with the page tree
Usage
vendor/bin/typo3 cleanup:orphanrecords [--dry-run]
Copied!
Options

--dry-run

--dry-run
If this option is set, the records will not be deleted. The command outputs a list of broken records
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

TYPO3 "pages" database table is a tree that represents a hierarchical structure with a set of connected nodes by their "uid" und "pid" values. All TCA records must be connected to a valid "pid".

This command finds and deletes all "pages" rows that do not have a proper tree connection to "pid" "0". It also finds and deletes TCA record rows having a "pid" set to invalid "pages" "uid"s.

The command can be called using "typo3 cleanup:orphanrecords -v --dry-run" to find affected records allowing manual inspection.

vendor/bin/typo3 cleanup:previewlinks

Find all versioned records and possibly cleans up invalid records in the database.
Usage
vendor/bin/typo3 cleanup:previewlinks
Copied!
Options

--silent

Do not output any message
Value
None allowed
Default value
false
Help

Look for preview links within the database table "sys_preview" that have been expired and and remove them. This command should be called regularly when working with workspaces.

vendor/bin/typo3 cleanup:versions

vendor/bin/typo3 cleanup:versions Back to list
Find all versioned records and possibly cleans up invalid records in the database.
Usage
vendor/bin/typo3 cleanup:versions [-p|--pid PID] [-d|--depth DEPTH] [--dry-run] [--action [ACTION]]
Copied!
Options

--pid

--pid / -p
Setting start page in page tree. Default is the page tree root, 0 (zero)
Value
Required

--depth

--depth / -d
Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.
Value
Required

--dry-run

--dry-run
If this option is set, the records will not actually be deleted/modified, but just the output which records would be touched are shown
Value
None allowed
Default value
false

--action

--action
Specify which action should be taken. Set it to "versions_in_live", "published_versions" or "invalid_workspace"
Value
Optional

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Traverse page tree and find versioned records. Also list all versioned records, additionally with some inconsistencies in the database, which can cleaned up with the "action" option. If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 extension:list

vendor/bin/typo3 extension:list Back to list
Shows the list of extensions available to the system
Usage
vendor/bin/typo3 extension:list [-a|--all] [-i|--inactive]
Copied!
Options

--all

--all / -a
Also display currently inactive/uninstalled extensions.
Value
None allowed
Default value
false

--inactive

--inactive / -i
Only show inactive/uninstalled extensions available for installation.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Shows the list of extensions available to the system

vendor/bin/typo3 extension:setup

vendor/bin/typo3 extension:setup Back to list
Set up extensions
Usage
vendor/bin/typo3 extension:setup [-e|--extension EXTENSION]
Copied!
Options

--extension

--extension / -e
Only set up extensions with given key
Value
Required (multiple)
Default value
[]

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Setup all extensions or the given extension by extension key. This must be performed after new extensions are required via Composer.

The command performs all necessary setup operations, such as database schema changes, static data import, distribution files import etc.

The given extension keys must be recognized by TYPO3 or will be ignored.

vendor/bin/typo3 fluid:schema:generate

vendor/bin/typo3 fluid:schema:generate Back to list
Generate XSD schema files for all available ViewHelpers in var/transient/
Usage
vendor/bin/typo3 fluid:schema:generate
Copied!
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Generate XSD schema files for all available ViewHelpers in var/transient/

vendor/bin/typo3 impexp:export

vendor/bin/typo3 impexp:export Back to list
Exports a T3D / XML file with content of a page tree
Usage
vendor/bin/typo3 impexp:export [--type [TYPE]] [--pid [PID]] [--levels [LEVELS]] [--table [TABLE]] [--record [RECORD]] [--list [LIST]] [--include-related [INCLUDE-RELATED]] [--include-static [INCLUDE-STATIC]] [--exclude [EXCLUDE]] [--exclude-disabled-records] [--exclude-html-css] [--title [TITLE]] [--description [DESCRIPTION]] [--notes [NOTES]] [--dependency [DEPENDENCY]] [--save-files-outside-export-file] [--] [<filename>]
Copied!
Arguments

filename

filename
The filename to export to (without file extension).
Options

--type

--type
The file type (xml, t3d, t3d_compressed).
Value
Optional
Default value
"xml"

--pid

--pid
The root page of the exported page tree.
Value
Optional
Default value
-1

--levels

--levels
The depth of the exported page tree. "-2": "Records on this page", "0": "This page", "1": "1 level down", .. "999": "Infinite levels".
Value
Optional
Default value
0

--table

--table
Include all records of this table. Examples: "_ALL", "tt_content", "sys_file_reference", etc.
Value
Optional (multiple)
Default value
[]

--record

--record
Include this specific record. Pattern is "{table}:{record}". Examples: "tt_content:12", etc.
Value
Optional (multiple)
Default value
[]

--list

--list
Include the records of this table and this page. Pattern is "{table}:{pid}". Examples: "be_users:0", etc.
Value
Optional (multiple)
Default value
[]

--include-related

Include record relations to this table, including the related record. Examples: "_ALL", "sys_category", etc.
Value
Optional (multiple)
Default value
[]

--include-static

--include-static
Include record relations to this table, excluding the related record. Examples: "_ALL", "be_users", etc.
Value
Optional (multiple)
Default value
[]

--exclude

--exclude
Exclude this specific record. Pattern is "{table}:{record}". Examples: "fe_users:3", etc.
Value
Optional (multiple)
Default value
[]

--exclude-disabled-records

--exclude-disabled-records
Exclude records which are handled as disabled by their TCA configuration, e.g. by fields "disabled", "starttime" or "endtime".
Value
None allowed
Default value
false

--exclude-html-css

--exclude-html-css
Exclude referenced HTML and CSS files.
Value
None allowed
Default value
false

--title

--title
The meta title of the export.
Value
Optional

--description

--description
The meta description of the export.
Value
Optional

--notes

--notes
The meta notes of the export.
Value
Optional

--dependency

--dependency
This TYPO3 extension is required for the exported records. Examples: "news", "powermail", etc.
Value
Optional (multiple)
Default value
[]

--save-files-outside-export-file

--save-files-outside-export-file
Save files into separate folder instead of including them into the common export file. Folder name pattern is "{filename}.files".
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Exports a T3D / XML file with content of a page tree

vendor/bin/typo3 impexp:import

vendor/bin/typo3 impexp:import Back to list
Imports a T3D / XML file with content into a page tree
Usage
vendor/bin/typo3 impexp:import [--update-records] [--ignore-pid] [--force-uid] [--import-mode [IMPORT-MODE]] [--enable-log] [--] <file> [<pid>]
Copied!
Arguments

file

file
The file path to import from (.t3d or .xml).

pid

pid
The page to import to.
Options

--update-records

--update-records
If set, existing records with the same UID will be updated instead of inserted.
Value
None allowed
Default value
false

--ignore-pid

--ignore-pid
If set, page IDs of updated records are not corrected (only works in conjunction with --update-records).
Value
None allowed
Default value
false

--force-uid

--force-uid
If set, UIDs from file will be forced.
Value
None allowed
Default value
false

--import-mode

--import-mode
Set the import mode of this specific record. Pattern is "{table}:{record}={mode}". Available modes for new records are "force_uid" and "exclude" and for existing records "as_new", "ignore_pid", "respect_pid" and "exclude". Examples are "pages:987=force_uid", "tt_content:1=as_new", etc.
Value
Optional (multiple)
Default value
[]

--enable-log

--enable-log
If set, all database actions are logged.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Imports a T3D / XML file with content into a page tree

vendor/bin/typo3 language:update

vendor/bin/typo3 language:update Back to list
Update the language files of all activated extensions
Usage
vendor/bin/typo3 language:update [--no-progress] [--fail-on-warnings] [--skip-extension SKIP-EXTENSION] [--] [<locales>...]
Copied!
Arguments

locales

locales
Provide iso codes separated by space to update only selected language packs. Example `bin/typo3 language:update de ja`.
Options

--no-progress

--no-progress
Disable progress bar.
Value
None allowed
Default value
false

--fail-on-warnings

--fail-on-warnings
Fail command when translation was not found on the server.
Value
None allowed
Default value
false

--skip-extension

--skip-extension
Skip extension. Useful for e.g. for not public extensions, which don't have language packs.
Value
Required (multiple)
Default value
[]

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Update the language files of all activated extensions

vendor/bin/typo3 lint:yaml

vendor/bin/typo3 lint:yaml Back to list
Lint a YAML file and outputs encountered errors
Usage
vendor/bin/typo3 lint:yaml [--format FORMAT] [--exclude EXCLUDE] [--parse-tags|--no-parse-tags] [--] [<filename>...]
Copied!
Arguments

filename

filename
A file, a directory or "-" for reading from STDIN
Options

--format

--format
The output format ("txt", "json", "github")
Value
Required

--exclude

--exclude
Path(s) to exclude
Value
Required (multiple)
Default value
[]

--parse-tags

--parse-tags
Parse custom tags
Value
None allowed

--no-parse-tags

--no-parse-tags
Negate the "--parse-tags" option
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The lint:yaml command lints a YAML file and outputs to STDOUT the first encountered syntax error.

You can validates YAML contents passed from STDIN:

cat filename | php vendor/vendor/bin/typo3 lint:yaml -
Copied!

You can also validate the syntax of a file:

php vendor/vendor/bin/typo3 lint:yaml filename
Copied!

Or of a whole directory:

php vendor/vendor/bin/typo3 lint:yaml dirname
Copied!

The --format option specifies the format of the command output:

php vendor/vendor/bin/typo3 lint:yaml dirname --format=json
Copied!

You can also exclude one or more specific files:

php vendor/vendor/bin/typo3 lint:yaml dirname --exclude="dirname/foo.yaml" --exclude="dirname/bar.yaml"
Copied!

vendor/bin/typo3 mailer:spool:send

vendor/bin/typo3 mailer:spool:send Back to list
Sends emails from the spool
Usage
vendor/bin/typo3 mailer:spool:send [--message-limit MESSAGE-LIMIT] [--time-limit TIME-LIMIT] [--recover-timeout RECOVER-TIMEOUT]
Copied!
vendor/bin/typo3 swiftmailer:spool:send
Copied!
Options

--message-limit

--message-limit
The maximum number of messages to send.
Value
Required

--time-limit

--time-limit
The time limit for sending messages (in seconds).
Value
Required

--recover-timeout

--recover-timeout
The timeout for recovering messages that have taken too long to send (in seconds).
Value
Required

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Sends emails from the spool

vendor/bin/typo3 messenger:consume

vendor/bin/typo3 messenger:consume Back to list
Consume messages
Usage
vendor/bin/typo3 messenger:consume [--sleep SLEEP] [--queues QUEUES] [--exit-code-on-limit EXIT-CODE-ON-LIMIT] [--] [<receivers>...]
Copied!
Arguments

receivers

receivers
Names of the receivers/transports to consume in order of priority
Options

--sleep

--sleep
Seconds to sleep before asking for new messages after no messages were found
Value
Required
Default value
1

--queues

--queues
Limit receivers to only consume from the specified queues
Value
Required (multiple)
Default value
[]

--exit-code-on-limit

--exit-code-on-limit
Exit code when limits are reached
Value
Required
Default value
0

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The messenger:consume command consumes messages and dispatches them to the message bus.

php vendor/vendor/bin/typo3 messenger:consume <receiver-name>

To receive from multiple transports, pass each name:

php vendor/vendor/bin/typo3 messenger:consume receiver1 receiver2
Copied!

Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):

php vendor/vendor/bin/typo3 messenger:consume <receiver-name> --queues=fasttrack
Copied!

vendor/bin/typo3 redirects:checkintegrity

vendor/bin/typo3 redirects:checkintegrity Back to list
Check integrity of redirects
Usage
vendor/bin/typo3 redirects:checkintegrity [<site>]
Copied!
Arguments

site

site
If set, then only pages of a specific site are checked
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Check integrity of redirects

vendor/bin/typo3 redirects:cleanup

vendor/bin/typo3 redirects:cleanup Back to list
Cleanup old redirects periodically for given constraints like days, hit count or domains.
Usage
vendor/bin/typo3 redirects:cleanup [-d|--domain [DOMAIN]] [-s|--statusCode [STATUSCODE]] [-a|--days [DAYS]] [-c|--hitCount [HITCOUNT]] [-p|--path [PATH]] [-t|--creationType [CREATIONTYPE]] [-i|--integrityStatus [INTEGRITYSTATUS]]
Copied!
Options

--domain

--domain / -d
Cleanup redirects matching provided domain(s)
Value
Optional (multiple)
Default value
[]

--statusCode

--statusCode / -s
Cleanup redirects matching provided status code(s)
Value
Optional (multiple)
Default value
[]

--days

--days / -a
Cleanup redirects older than provided number of days
Value
Optional

--hitCount

--hitCount / -c
Cleanup redirects matching hit counts lower than given number
Value
Optional

--path

--path / -p
Cleanup redirects matching given path (as database like expression)
Value
Optional

--creationType

--creationType / -t
Cleanup redirects matching provided creation type
Value
Optional

--integrityStatus

--integrityStatus / -i
Cleanup redirects matching provided integrity status
Value
Optional

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Cleanup old redirects periodically for given constraints like days, hit count or domains.

vendor/bin/typo3 referenceindex:update

vendor/bin/typo3 referenceindex:update Back to list
Update the reference index of TYPO3
Usage
vendor/bin/typo3 referenceindex:update [-c|--check]
Copied!
Options

--check

--check / -c
Only check the reference index of TYPO3
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Update the reference index of TYPO3

vendor/bin/typo3 scheduler:execute

vendor/bin/typo3 scheduler:execute Back to list
Execute given Scheduler tasks.
Usage
vendor/bin/typo3 scheduler:execute [-t|--task [TASK]]
Copied!
Options

--task

--task / -t
Execute tasks by given id. To run all tasks of a group prefix the group id with "g:", e.g. "g:1"
Value
Optional (multiple)
Default value
[]

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Execute given Scheduler tasks.

vendor/bin/typo3 scheduler:list

vendor/bin/typo3 scheduler:list Back to list
List all Scheduler tasks.
Usage
vendor/bin/typo3 scheduler:list [-g|--group [GROUP]] [-w|--watch [WATCH]]
Copied!
Options

--group

--group / -g
Show only groups with given uid
Value
Optional (multiple)
Default value
[]

--watch

--watch / -w
Start watcher mode (polling)
Value
Optional

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

List all Scheduler tasks.

vendor/bin/typo3 scheduler:run

vendor/bin/typo3 scheduler:run Back to list
Start the TYPO3 Scheduler from the command line.
Usage
vendor/bin/typo3 scheduler:run [-i|--task [TASK]] [-f|--force] [-s|--stop]
Copied!
Options

--task

--task / -i
UID of a specific task. Can be provided multiple times to execute multiple tasks sequentially.
Value
Optional (multiple)
Default value
[]

--force

--force / -f
Force execution of the task which is passed with --task option
Value
None allowed
Default value
false

--stop

--stop / -s
Stop the task which is passed with --task option
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

If no parameter is given, the scheduler executes any tasks that are overdue to run. Call it like this: typo3/sysext/core/vendor/bin/typo3 scheduler:run --task=13 -f

vendor/bin/typo3 setup:begroups:default

vendor/bin/typo3 setup:begroups:default Back to list
Setup default backend user groups
Usage
vendor/bin/typo3 setup:begroups:default [-n|--no-interaction] [-g|--groups [GROUPS]] [-f|--force]
Copied!
Options

--groups

--groups / -g
Which backend user groups do you want to create? [ Editor, Advanced Editor, Both, None]
Value
Optional
Default value
"Both"

--force

--force / -f
Force creating a new group with the same name, even if a group with that name already exists.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

The command will allow you to create base backend user groups for your TYPO3 installation.

You can create either both or one of the following groups:

  • Editor
  • Advanced Editor

vendor/bin/typo3 site:list

vendor/bin/typo3 site:list Back to list
Shows the list of sites available to the system
Usage
vendor/bin/typo3 site:list
Copied!
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Shows the list of sites available to the system

vendor/bin/typo3 site:sets:list

vendor/bin/typo3 site:sets:list Back to list
Shows the list of available site sets
Usage
vendor/bin/typo3 site:sets:list [-a|--all]
Copied!
Options

--all

--all / -a
Show all sets, including hidden ones.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Shows the list of available site sets

vendor/bin/typo3 site:show

vendor/bin/typo3 site:show Back to list
Shows the configuration of the specified site
Usage
vendor/bin/typo3 site:show <identifier>
Copied!
Arguments

identifier

identifier
The identifier of the site
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Shows the configuration of the specified site

vendor/bin/typo3 syslog:list

vendor/bin/typo3 syslog:list Back to list
Show entries from the sys_log database table of the last 24 hours.
Usage
vendor/bin/typo3 syslog:list
Copied!
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Prints a list of recent sys_log entries. If you want to get more detailed information, use the --verbose option.

vendor/bin/typo3 upgrade:list

vendor/bin/typo3 upgrade:list Back to list
List available upgrade wizards.
Usage
vendor/bin/typo3 upgrade:list [-a|--all]
Copied!
Options

--all

--all / -a
Include wizards already done.
Value
None allowed
Default value
false

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

List available upgrade wizards.

vendor/bin/typo3 upgrade:mark:undone

vendor/bin/typo3 upgrade:mark:undone Back to list
Mark upgrade wizard as undone.
Usage
vendor/bin/typo3 upgrade:mark:undone <wizardIdentifier>
Copied!
Arguments

wizardIdentifier

wizardIdentifier
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Mark upgrade wizard as undone.

vendor/bin/typo3 upgrade:run

vendor/bin/typo3 upgrade:run Back to list
Run upgrade wizard. Without arguments all available wizards will be run.
Usage
vendor/bin/typo3 upgrade:run [<wizardName>]
Copied!
Arguments

wizardName

wizardName
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

This command allows running upgrade wizards on CLI. To run a single wizard add the identifier of the wizard as argument. The identifier of the wizard is the name it is registered with in ext_localconf.

vendor/bin/typo3 workspace:autopublish

vendor/bin/typo3 workspace:autopublish Back to list
Publish a workspace with a publication date.
Usage
vendor/bin/typo3 workspace:autopublish
Copied!
Options

--silent

--silent
Do not output any message
Value
None allowed
Default value
false
Help

Some workspaces can have an auto-publish publication date to put all "ready to publish" content online on a certain date.

Writing custom commands 

TYPO3 uses the Symfony Console component to define and execute command-line interface (CLI) commands. Custom commands allow extension developers to provide their own functionality for use on the command line or in the TYPO3 scheduler.

The custom command class 

To implement a console command in TYPO3 extend the \Symfony\Component\Console\Command\Command class.

Console command registration 

There are two ways that a console command can be registered: you can use the PHP Attribute AsCommand or register the command in your Services.yaml:

PHP attribute AsCommand 

CLI commands can be registered by setting the attribute \Symfony\Component\Console\Attribute\AsCommand in the command class. When using this attribute there is no need to register the command in Services.yaml.

#[AsCommand(
    name: 'examples:dosomething',
    description: 'A command that does nothing and always succeeds.',
    aliases: ['examples:dosomethingalias'],
)]
Copied!

The following parameters are available:

name
The name under which the command is available.
description
Gives a short description. It will be displayed in the list of commands and the help information for the command.
hidden
Hide the command from the command list by setting hidden to true.
alias
A command can be made available under a different name. Set to true if your command name is an alias.

If you want to set a command as non-schedulable it has to be registered via tag not attribute.

Tag console.command in the Services.yaml 

You can register the command in Configuration/Services.yaml by adding the service definition of your class as a tag console.command:

packages/my_extension/Configuration/Services.yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and always succeeds.'
      # Also an alias for the command can be configured
      - name: console.command
        command: 'examples:dosomethingalias'
        alias: true
Copied!

Making a command non-schedulable 

A command can be set as disabled for the scheduler by setting schedulable to false. This can only be done when registering the command via tag and not via attribute:

packages/my_extension/Configuration/Services.yaml
services:
  # ...

  MyVendor\MyExtension\Command\DoSomethingCommand:
    tags:
      - name: console.command
        command: 'examples:dosomething'
        description: 'A command that does nothing and cannot be scheduled.'
        schedulable: false
Copied!

Context of a command: No request, no site, no user 

Commands are called from the console / command line and not through a web request. Therefore, when the code of your custom command is run by default there is no ServerRequest available, no backend or frontend user logged in and a request is called without context of a site or page.

For that reason Site Settings, TypoScript and TSconfig are not loaded by default, Extbase repositories cannot be used without taking precautions and there are many more limitations.

Extbase limitations in CLI context 

Extbase relies on frontend TypoScript, and features such as request-based TypoScript conditions may not behave as expected.

Instead, use the Query Builder or DataHandler when implementing custom commands.

Using the DataHandler in CLI commands 

When using the DataHandler in a CLI command, backend user authentication is required. For more information see: Using the DataHandler in a Symfony command.

Initialize backend user 

A backend user can be initialized inside the execute() method as follows:

packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.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\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class DoBackendRelatedThingsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();
        // Do backend related stuff

        return Command::SUCCESS;
    }
}
Copied!

This is necessary when using the DataHandler or other backend permission-handling-related tasks.

Simulating a frontend request in TYPO3 Commands 

Executing a TYPO3 command in the CLI does not trigger a frontend (web) request. This means that several request attributes required for link generation via Fluid or TypoScript are missing by default. While setting the site attribute in the request is a first step, it does not fully replicate the frontend behavior.

A minimal request configuration may be sufficient for generating simple links or using FluidEmail:

packages/my_extension/Classes/Command/DoBackendRelatedThingsCommand.php
<?php

declare(strict_types=1);

namespace T3docs\Examples\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Mail\FluidEmail;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Site\SiteFinder;

#[AsCommand(
    name: 'myextension:sendmail',
)]
class SendFluidMailCommand extends Command
{
    public function __construct(
        private readonly SiteFinder $siteFinder,
        private readonly MailerInterface $mailer,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();

        // The site has to have a fully qualified domain name
        $site = $this->siteFinder->getSiteByPageId(1);
        $request = (new ServerRequest())
            ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
            ->withAttribute('site', $site);
        $GLOBALS['TYPO3_REQUEST'] = $request;
        // Send some mails with FluidEmail
        $email = new FluidEmail();
        $email->setRequest($request);
        // Set receiver etc
        $this->mailer->send($email);
        return Command::SUCCESS;
    }
}
Copied!

Create a command with arguments and interaction 

Passing arguments 

Since a command extends \Symfony\Component\Console\Command\Command, it is possible to define arguments (ordered) and options (unordered) using the Symfony command API. This is explained in depth on the following Symfony Documentation page:

Both arguments and properties can be registered in a command implementation by overriding the configure() method. You can call methods addArgument() and addOption() to register them.

This argument can be retrieved with $input->getArgument(), the options with $input->getOption(), for example:

packages/my_extension/Classes/Command/SendFluidMailCommand.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\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use T3docs\Examples\Exception\InvalidWizardException;

#[AsCommand(
    name: 'myextension:createwizard',
)]
final class CreateWizardCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->setHelp('This command accepts arguments')
            ->addArgument(
                'wizardName',
                InputArgument::OPTIONAL,
                'The wizard\'s name',
            )
            ->addOption(
                'brute-force',
                'b',
                InputOption::VALUE_NONE,
                'Allow the "Wizard of Oz". You can use --brute-force or -b when running command',
            );
    }
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $wizardName = $input->getArgument('wizardName');
        $bruteForce = (bool)$input->getOption('brute-force');
        try {
            $this->doMagic($io, $wizardName, $bruteForce);
        } catch (InvalidWizardException) {
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }

    private function doMagic(SymfonyStyle $io, mixed $wizardName, bool $bruteForce): void
    {
        // do your magic here
    }
}
Copied!

User interaction on the console 

You can create a SymfonyStyle console user interface using the $input and $output parameters of the execute() function:

packages/my_extension/Classes/Command/CrazyCalculatorCommand.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\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'myextension:crazycalculator',
)]
final class CrazyCalculatorCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $io->title('Welcome to our awesome extension');

        $io->text([
            'We will ask some questions.',
            'Please take your time to answer them.',
        ]);
        do {
            $number = (int)$io->ask(
                'Please enter a number greater 0',
                '42',
            );
        } while ($number <= 0);
        $operation = (string)$io->choice(
            'Chose the desired operation',
            ['squared', 'divided by 0'],
            'squared',
        );
        switch ($operation) {
            case 'squared':
                $io->success(sprintf('%d squared is %d', $number, $number * $number));
                return Command::SUCCESS;
            default:
                $io->error('Operation ' . $operation . 'is not supported. ');
                return Command::FAILURE;
        }
    }
}
Copied!

The $io variable can be used to generate output and prompt for input.

Dependency injection in console commands 

You can use dependency injection (DI) in console commands via constructor injection or method injection.

packages/my_extension/Classes/Command/MeowInformationCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use T3docs\Examples\Http\MeowInformationRequester;

#[AsCommand(
    name: 'myextension:dosomething',
)]
final class MeowInformationCommand extends Command
{
    public function __construct(
        private readonly MeowInformationRequester $requester,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        if (!$this->requester->isReady()) {
            $this->logger->error('MeowInformationRequester was not ready! ');
            return Command::SUCCESS;
        }
        // Do awesome stuff
        return Command::SUCCESS;
    }
}
Copied!

More about Symfony console commands 

Tutorial: Create a console command from scratch 

A console command is always inside an extension. If you want to create one, kickstart a custom extension or use your site package extension.

Creating a basic command 

In this section we will create an empty command skeleton with no parameters or user interaction.

Create a class called DoSomethingCommand which extends \Symfony\Component\Console\Command\Command.

EXT:my_extension/Classes/Command/MyCommand.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\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'myextension:dosomething',
    description: 'A command that does nothing and always succeeds.',
    aliases: ['examples:dosomethingalias'],
)]
class DoSomethingCommand extends Command
{
    protected function configure(): void
    {
        $this->setHelp('This command does nothing. It always succeeds.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->info('Command needs to be implemented. ');
        return Command::SUCCESS;
    }
}
Copied!

The following two methods should be overridden by your class:

configure()
As the name suggests, this is where the command can be configured. Add a help text and/or define arguments and options.
execute()
Contains the command logic. Must return an integer. It is considered best practice to return the constants Command::SUCCESS or Command::FAILURE.

The above example can be run via the command line. If a newly created or edited command is not found, clear the cache first:

vendor/bin/typo3 cache:flush
vendor/bin/typo3 examples:dosomething
Copied!
typo3/sysext/core/bin/typo3 cache:flush
typo3/sysext/core/bin/typo3 examples:dosomething
Copied!

The command will return without a message as it does nothing but state that it has succeeded.

Example console command implementations 

A command with parameters and arguments 

packages/my_extension/Classes/Command/SendFluidMailCommand.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\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use T3docs\Examples\Exception\InvalidWizardException;

#[AsCommand(
    name: 'myextension:createwizard',
)]
final class CreateWizardCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->setHelp('This command accepts arguments')
            ->addArgument(
                'wizardName',
                InputArgument::OPTIONAL,
                'The wizard\'s name',
            )
            ->addOption(
                'brute-force',
                'b',
                InputOption::VALUE_NONE,
                'Allow the "Wizard of Oz". You can use --brute-force or -b when running command',
            );
    }
    protected function execute(
        InputInterface $input,
        OutputInterface $output,
    ): int {
        $io = new SymfonyStyle($input, $output);
        $wizardName = $input->getArgument('wizardName');
        $bruteForce = (bool)$input->getOption('brute-force');
        try {
            $this->doMagic($io, $wizardName, $bruteForce);
        } catch (InvalidWizardException) {
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }

    private function doMagic(SymfonyStyle $io, mixed $wizardName, bool $bruteForce): void
    {
        // do your magic here
    }
}
Copied!

This command takes one argument wizardName (optional) and one option (optional), which can be added on the command line:

vendor/bin/typo3 examples:createwizard [-b] [wizardName]
Copied!
typo3/sysext/core/bin/typo3 examples:createwizard [-b] [wizardName]
Copied!

Sending a FluidMail via command 

packages/my_extension/Classes/Command/SendFluidMailCommand.php
<?php

declare(strict_types=1);

namespace T3docs\Examples\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Mail\FluidEmail;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Site\SiteFinder;

#[AsCommand(
    name: 'myextension:sendmail',
)]
class SendFluidMailCommand extends Command
{
    public function __construct(
        private readonly SiteFinder $siteFinder,
        private readonly MailerInterface $mailer,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        Bootstrap::initializeBackendAuthentication();

        // The site has to have a fully qualified domain name
        $site = $this->siteFinder->getSiteByPageId(1);
        $request = (new ServerRequest())
            ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
            ->withAttribute('site', $site);
        $GLOBALS['TYPO3_REQUEST'] = $request;
        // Send some mails with FluidEmail
        $email = new FluidEmail();
        $email->setRequest($request);
        // Set receiver etc
        $this->mailer->send($email);
        return Command::SUCCESS;
    }
}
Copied!

Content Elements & Plugins 

This chapter handles content elements & plugins: What they are, how they can be created, how existing content elements or plugins can be customized etc.

Introduction 

In TYPO3, Content elements and plugins are both stored as Database records in table tt_content. They are usually edited in the backend in module Web > Page.

Content elements and plugins are both used to present and manage content on a website, but they serve different purposes and have distinct characteristics:

Content element Text & Media

Content elements 

A content element is a standard unit for managing and displaying content, such as text, images, videos, tables, and more. TYPO3 provides a variety of built-in content elements. It is possible to define custom content elements.

Plugin news article detail

Plugins 

A plugin in TYPO3 is more complex, typically providing dynamic or interactive functionality. Plugins are usually provided by extensions that introduce new features to the website.

The data to be displayed is usually supplied by a special PHP class called a "controller". Depending on the technology used in the controller the plugin can be an Extbase plugin or a plain plugin.

Content elements in TYPO3 

A content element is a standard unit for managing and displaying content, such as text, images, videos, tables, and more.

In the TYPO3 backend, content elements are commonly managed in module Web > Page.

From a technical point of view content elements are records stored in the database table tt_content. Each content element has a specific content element type, specified by the database field tt_content.CType. This type influences both the backend form and the frontend output.

The appearance of a content element in the backend form is defined via the TYPO3 Configuration Array (TCA) of table tt_content. Each content element type is configured by one entry in the section $TCA['types'].

The output of the content element in the frontend is configured by an entry in the TypoScript top-level object tt_content using the same key as in TCA. In most cases a FLUIDTEMPLATE is used delegating the actual output to the templating engine Fluid.

A content element can be of a type supplied by TYPO3, such as textmedia (text with or without images or videos). Or it can have a custom type supplied by an extension such as carousel provided by the bk2k/bootstrap-package extension.

You can add custom content elements to your extension or site package.

It is also possible to use an extension such as contentblocks/content-blocks , mask/mask , or t3/dce to add custom content elements to your projects.

Adding custom content elements is possible without writing PHP code and can therefore also be done by TYPO3 integrators.

Plugins in TYPO3 

A plugin in TYPO3 is a more complex implementation, typically providing dynamic or interactive functionality. Plugins are usually provided by extensions that introduce new features to the website.

The data to be displayed is usually supplied by a special PHP class called a "controller". Depending on the technology used in the controller the plugin can be an Extbase plugin or a plain plugin.

Extbase plugins 

For usage in the TYPO3 backend Extbase plugins are registered with utility functions of class \TYPO3\CMS\Extbase\Utility\ExtensionUtility (not to be confused with \TYPO3\CMS\Core\Utility\ExtensionManagementUtility ).

An Extbase plugin is configured for the frontend with ExtensionUtility::configurePlugin() in file EXT:my_extension/ext_localconf.php:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);
defined('TYPO3') or die();

use MyVendor\MyExtension\Controler\MyController;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::configurePlugin(
    'MyExtension',
    'MyPlugin',
    [MyController::class => 'list,comment'],
    [MyController::class => 'comment'],
    ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
);
Copied!

Deprecated since version 13.4

Method ExtensionUtility::configurePlugin() also takes care of registering the plugin for frontend output in TypoScript using an object of type EXTBASEPLUGIN.

If it is desired that editors can insert the Extbase plugin like a content element into the page it also needs to be registered with ExtensionUtility::registerPlugin() in the TCA Overrides, for example file EXT:my_extension/Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_plugin.title',
    'myextension_pluginicon',
    'plugins',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_plugin.description',
);
Copied!

For a detailed explanation of Extbase plugins including examples for controllers see chapter Extbase: Extension framework in TYPO3.

Plugins without Extbase 

It is possible to create a plugin without using Extbase by creating a plain PHP class as a controller.

In this case you have to define the TypoScript configuration yourself. A USER or USER_INT TypoScript object can be used to delegate the rendering to your controller:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
plugin.tx_myextension_myplugin = USER_INT
plugin.tx_myextension_myplugin {
  userFunc = MyVendor\MyPlugin\Controller\MyController->doSomething
}

tt_content.myextension_myplugin < plugin.tx_myextension_myplugin
Copied!

To register such a plugin as content element you can use function ExtensionManagementUtility::addPlugin() in the TCA overrides, for example EXT:my_extension/Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addPlugin(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:my_plugin.title',
        'value' => 'myextension_myplugin',
        'group' => 'plugins',
        'icon' => 'myextension_mypluginicon',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:my_plugin.description',
    ],
    'CType',
    'my_extension',
);
Copied!

Deprecated since version 13.4

Typical characteristics of plugins 

  • Plugins often use additional database tables which contain records which are dynamically displayed via the plugin - often in a list view, a single view, optionally with pagination and search functionality. An extension may provide several plugins, each with a dedicated function, such as the list view.
  • Plugins are often used if more complex functionality is required (than in non- plugin content elements)
  • Plugins can be created using the Extbase framework or by Core functionality.

A typical extension with plugins is the georgringer/news extension which comes with plugins to display news records in lists or as a single view with only one news record.

The news records are stored in a custom database table (tx_news_domain_model_news) and can be edited in the backend.

There are also system extensions that have plugins. typo3/cms-felogin has a plugin that allow frontend users, stored in table fe_users to log into the website. typo3/cms-indexed-search has a plugin that can be used to search in the index and display search results.

Editing 

The Editors Tutorial describes how to work with page content and lists the basic TYPO3 content elements and how to work with them.

Additional descriptions can be found in the fluid_styled_content documentation.

Customizing 

Backend Layouts can be configured to define how content elements are arranged in the TYPO3 backend (in rows, columns, grids). This can be used in the frontend to determine how the content elements are to be arranged (e.g. in the footer of the page, left column etc.).

Often content elements and plugins contain a number of fields. Not all of these may be relevant for your site. It is good practice to configure which fields will be displayed in the backend. There are a number of ways to do this:

Creating custom content element types or plugins 

The following chapters handle how to create custom content element types and plugins:

How to make your plugins or content elements configurable by editors with

Create a custom content element type 

This page explains how to create your own custom content element types. These are comparable to the predefined content element types supplied by TYPO3. The latter can be found in the system extension fluid_styled_content.

A content element can be based on fields already available in the tt_content table.

It is also possible to add extra fields to the tt_content table, see Extending tt_content.

The data of the content element is then passed to a TypoScript object, in most cases to a FLUIDTEMPLATE.

Some data might need additional Data processing. Data processors are frequently used for example to process files (files data processor) or to fetch related records (database-query data processor).

A data processor can also be used to convert a string to an array, as is done for example in the table content element with the field bodytext.

In these cases Fluid does not have to deal with these manipulations or transformation.

You can find the example below in the TYPO3 Documentation Team extension t3docs/examples .

Prerequisites 

The following examples require the system extension fluid_styled_content.

It can be installed via Composer with:

composer req typo3/cms-fluid-styled-content
Copied!

Use an extension 

We recommend to create your own extension for new custom content element types. The following example uses the extension key examples.

Here you can find information on how to create an extension.

Register the content element type 

First we need to define the key of the new content element type. We use myextension_basiccontent throughout the simple example.

Next the key needs to be added to the select field CType. This will make it available in Type dropdown in the backend.

The following call needs to be added to the file Configuration/TCA/Overrides/tt_content.php.

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

Now the new content element is available in the CType selector and the "New Content Element" wizard.

Display an icon 

If you define no icon a default icon will be displayed.

You can use an existing icon from the TYPO3 core or register your own icon using the Icon API.

 ExtensionManagementUtility::addTcaSelectItem(
     'tt_content',
     'CType',
     [
         'label' => 'Example - basic content',
         'value' => $key,
+        'icon' => 'examples-icon-basic',
         'group' => 'default',
     ],
     'textmedia',
     'after',
 );
+$GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes'][$key] = 'examples-icon-basic';
Copied!

The new content element wizard 

Changed in version 13.0

Starting with TYPO3 13.0 content elements added via TCA are automatically displayed in the New Content Element wizard. To stay compatible with both TYPO3 v12.4 and v13 keep the page TSconfig for TYPO3 v12.4. See New Content Element wizard in TYPO3 12.4.

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

The values in the array highlighted in the code example above are used for the display in the New Content Element wizard.

It is also possible to define a description and an icon:

 ExtensionManagementUtility::addTcaSelectItem(
     'tt_content',
     'CType',
     [
         'label' => 'Example - basic content',
         'value' => $key,
+        'icon' => 'examples-icon-basic',
+        'description' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:basic_content.description',
         'group' => 'default',
     ],
     'textmedia',
     'after',
 );
Copied!

The content element wizard configuration is described in detail in New content element wizard.

Configure the backend form 

Changed in version 13.3

Configure the backend fields for your new content element in the file Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

$key = 'myextension_basiccontent';

// Adds the content element to the "Type" dropdown
ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'label' => 'Example - basic content',
        'value' => $key,
        'group' => 'default',
    ],
    'textmedia',
    'after',
);

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types'][$key] = [
    'showitem' => '
            --palette--;;headers,
            bodytext,
        ',
];
Copied!

In line 27 a custom palette with the header and related fields is displayed. This palette and its fields are defined in EXT:frontend/Configuration/TCA/tt_content.php (GitHub).

In line 28 a predefined field, bodytext is added to be displayed in the form of the new content element type.

Configure the frontend rendering 

The output in the frontend gets configured in the setup TypoScript. See Add TypoScript to your extension about how to add TypoScript.

In the examples extension the TypoScript can be found at Configuration/TypoScript/CustomContentElements/Basic.typoscript

The Fluid templates for our custom content element will be saved in our extension. Therefore we need to add the path to the Properties:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
lib.contentElement {
  templateRootPaths.200 = EXT:my_extension/Resources/Private/Templates/ContentElements/
}
Copied!

You can use any index (200 in this example), just make sure it is unique. If needed you can also add paths for partials and layouts.

Now you can register the rendering of your custom content element:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
tt_content {
  myextension_basiccontent =< lib.contentElement
  myextension_basiccontent {
    templateName = BasicContent
  }
}
Copied!

The lib.contentElement path is defined in file EXT:fluid_styled_content/Configuration/TypoScript/Helper/ContentElement.typoscript (GitHub). and uses a FLUIDTEMPLATE.

We reference fluid_styled_content lib.contentElement from our new content element and only change the Fluid template to be used.

The Fluid template is configured by the Properties property as NewContentElement.

This will load a BasicContent.html template file from the path defined at the templateRootPaths.

In the example extension you can find the file at EXT:examples/Resources/Private/Templates/ContentElements/BasicContent.html

tt_content fields can now be used in the Fluid template by accessing them via the data variable.

The following example shows the text entered in the text field. New lines are converted to <br> tags.

EXT:examples/Resources/Private/Templates/NewContentElement.html
<html data-namespace-typo3-fluid="true"
      xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
   <h2>Data available to the content element: </h2>
   <f:debug inline="true">{_all}</f:debug>
   <h2>Output</h2>
   <div><f:format.html>{data.bodytext}</f:format.html></div>
</html>
Copied!

All fields of the table tt_content are now available in the variable data. Read more about Fluid.

Below you can see the example output of the new content element and a dump of all available data:

The example output

Extended example: Extend tt_content and use data processing 

You can find the complete example in the TYPO3 Documentation Team extension t3docs/examples . The steps for creating a simple new content element as above need to be repeated. We use the key examples_newcontentcsv in this example.

We want to output comma separated values (CSV) stored in the field bodytext. As different programs use different separators to store CSV we want to make the separator configurable.

Extending tt_content 

If the available fields in the table tt_content are not sufficient you can add your own fields. In this case we need a field tx_examples_separator from which to choose the desired separator.

Extending the database schema 

First we extend the database schema by adding the following to the file ext_tables.sql:

EXT:my_extension/ext_tables.sql
CREATE TABLE tt_content (
    myextension_separator varchar(255) DEFAULT '' NOT NULL,
    myextension_reference  int(11) unsigned DEFAULT '0' NOT NULL,
);
Copied!

Defining the field in the TCA 

The new field tx_examples_separator is added to the TCA definition of the table tt_content in the file Configuration/TCA/Overrides/tt_content.php:

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

declare(strict_types=1);
defined('TYPO3') or die();

$temporaryColumn = [
    'myextension_separator' => [
        'exclude' => 0,
        'label' => 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.myextension_separator',
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                ['Standard CSV data formats', '--div--'],
                ['Comma separated', ','],
                ['Semicolon separated', ';'],
                ['Special formats', '--div--'],
                ['Pipe separated (TYPO3 tables)', '|'],
                ['Tab separated', "\t"],
            ],
            'default' => ',',
        ],
    ],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $temporaryColumn);
Copied!

You can read more about defining fields via TCA in the TCA Reference.

Now the new field can be used in your Fluid template just like any other tt_content field.

Another example shows the connection to a foreign table. This allows you to be more flexible with the possible values in the select box. The new field myextension_reference is a reference to another table of the extension called tx_myextension_mytable:

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

declare(strict_types=1);
defined('TYPO3') or die();

$temporaryColumn = [
    'myextension_reference' => [
        'exclude' => 0,
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:' .
            'tt_content.myextension_reference',
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                ['None', '0'],
            ],
            'foreign_table' => 'tx_myextension_mytable',
            'foreign_table_where' =>
                'AND {#tx_myextension_mytable}.{#pid} = ###PAGE_TSCONFIG_ID### ' .
                'AND {#tx_myextension_mytable}.{#hidden} = 0 ' .
                'AND {#tx_myextension_mytable}.{#deleted} = 0 ' .
                'ORDER BY sys_category.uid',
            'default' => '0',
        ],
    ],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $temporaryColumn);
Copied!

Defining the field in the TCE 

An individual modification of the newly added field myextension_reference to the TCA definition of the table tt_content can be done in the TYPO3 Core Engine (TCE) page TSconfig. In most cases it is necessary to set the page id of the general storage folder. Then the examples extension will only use the content records from the given page id.

EXT:my_extension/Configuration/page.tsconfig
TCEFORM.tt_content.myextension_reference.PAGE_TSCONFIG_ID = 18
Copied!

If more than one page id is allowed, this configuration must be used instead (and the above TCA must be modified to use the marker ###PAGE_TSCONFIG_IDLIST### instead of ###PAGE_TSCONFIG_ID###):

EXT:my_extension/Configuration/page.tsconfig
TCEFORM.tt_content.myextension_reference.PAGE_TSCONFIG_IDLIST = 18, 19, 20
Copied!

Data processing 

Data processors can be used for data manipulation or fetching before the variables get passed on to the template.

This is done in the dataProcessing section where you can add an arbitrary number of data processors.

You can see a complete list of available data processors in the Typoscript Reference or write a custom data processor.

Each processor has to be added with a fully qualified class name and optional parameters to be used in the data processor:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
tt_content {
  myextension_newcontentcsv =< lib.contentElement
  myextension_newcontentcsv {
    templateName = DataProcCsv
    dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\CommaSeparatedValueProcessor
    dataProcessing.10 {
      fieldName = bodytext
      fieldDelimiter.field = myextension_separator
      fieldEnclosure = "
      maximumColumns.field = imagecols
      as = myTable
    }
  }
}
Copied!

You can now iterate over the variable myTable in the Fluid template, in this example Resources/Private/Templates/ContentElements/DataProcCsv.html

EXT:examples/Resources/Private/Templates/ContentElements/DataProcCsv.html
<html data-namespace-typo3-fluid="true" xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers">
   <h2>Data in variable "myTable"</h2>
   <f:debug inline="true">{myTable}</f:debug>

   <h2>Output, {data.imagecols} columns separated by char {data.tx_examples_separator}</h2>
   <table class="table table-hover">
      <f:for each="{myTable}" as="columns" iteration="i">
         <tr>
            <th scope="row">{i.cycle}</th>
            <f:for as="column" each="{columns}">
               <td>{column}</td>
            </f:for>
         <tr>
      </f:for>
   </table>
</html>
Copied!

The output would look like this (we added a debug of the variable myTable):

Output of the CommaSeparatedValueProcessor

Custom data processors 

When there is no suitable data processor that prepares the variables needed for your content element or template, you can define a custom data processor by implementing \TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface .

You can find the example below in the TYPO3 Documentation Team extension t3docs/examples .

Using a custom data processor in TypoScript 

The data processor can be configured through a TypoScript setup configuration. A custom data processor can be used in the definition of a "new custom content element" as follows:

EXT:examples/Configuration/TypoScript/DataProcessors/Processors/CustomCategoryProcessor.typoscript
tt_content {
    examples_dataproccustom =< lib.contentElement
    examples_dataproccustom {
        templateName = DataProcCustom
        dataProcessing.10 = custom-category
        dataProcessing.10 {
            as = categories
            categoryList.field = categories
        }
    }
}
Copied!

In the extension examples you can find the code in EXT:examples/Configuration/TypoScript/DataProcessors/Processors/CustomCategoryProcessor.typoscript.

In the field categories the comma-separated categories are stored.

Register an alias for the data processor (optional) 

New in version 12.1

Instead of using the fully-qualified class name as data processor identifier (in the example above \T3docs\Examples\DataProcessing\CustomCategoryProcessor) you can also define a short alias in Configuration/Services.yaml:

EXT:examples/Configuration/Services.yaml
T3docs\Examples\DataProcessing\CustomCategoryProcessor:
    tags:
        - name: 'data.processor'
          identifier: 'custom-category'
Copied!

The alias custom-category can now be used as data processor identifier like in the TypoScript example above.

Implementing the custom data processor 

The custom data processor must implement \TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface . The main method process() gets called with the following parameters:

ContentObjectRenderer $cObj
Receives the data of the current TypoScript context, in this case the data of the calling content element.
array $contentObjectConfiguration
Contains the configuration of the calling content element. In this example it is the configuration tt_content.examples_dataproccustom
array $processorConfiguration
Contains the configuration of the currently called data processor. In this example it is the value of as and the stdWrap configuration of the categoryList
array $processedData
On calling, contains the processed data of all previously called data processors on this same content element. Your custom data processor also stores the variables to be sent to the Fluid template here.

This is an example implementation of a custom data processor:

EXT:examples/Classes/DataProcessing/CustomCategoryProcessor.php
<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project. [...]
 */

namespace T3docs\Examples\DataProcessing;

use T3docs\Examples\Domain\Repository\CategoryRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

/**
 * Class for data processing comma separated categories
 */
class CustomCategoryProcessor implements DataProcessorInterface
{
    /**
     * Process data for the content element "My new content element"
     *
     * @param ContentObjectRenderer $cObj The data of the content element or page
     * @param array<string, mixed> $contentObjectConfiguration The configuration of Content Object
     * @param array<string, mixed> $processorConfiguration The configuration of this processor
     * @param array<string, mixed> $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
     * @return array<mixed> the processed data as key/value store
     */
    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData,
    ) {
        if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
            return $processedData;
        }
        // categories by comma separated list
        $categoryIdList = $cObj->stdWrapValue('categoryList', $processorConfiguration);
        $categories = [];
        if ($categoryIdList) {
            $categoryIdList = GeneralUtility::intExplode(',', (string)$categoryIdList, true);
            /** @var CategoryRepository $categoryRepository */
            $categoryRepository = GeneralUtility::makeInstance(CategoryRepository::class);
            foreach ($categoryIdList as $categoryId) {
                $categories[] = $categoryRepository->findByUid($categoryId);
            }
            // set the categories into a variable, default "categories"
            $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'categories');
            $processedData[$targetVariableName] = $categories;
        }
        return $processedData;
    }
}
Copied!

In the extension examples you can find the code in EXT:/examples/Classes/DataProcessing/CustomCategoryProcessor.php.

On being called, the CustomCategoryProcessor runs stdWrap on the calling ContentObjectRenderer, which has the data of the table tt_content in the calling content element.

The field categoryList gets configured in TypoScript as follows:

categoryList.field = categories
Copied!

stdWrap fetches the value of categoryList from tt_content.tx_examples_main_category of the currently calling content element.

Now the custom data processor processes the comma-separated values into an array of integers that represent uids of the table sys_category. It then fetches the category data from the CategoryRepository by calling findByUid.

The data of the category records then get stored in the desired key in the $processedData array.

To make the data processor more configurable, we test for a TypoScript if condition at the beginning, and name the key we use to store the data configurable by the configuration as.

Create plugins 

How to create plugins with the Extbase framework and Fluid templating engine is handled in depth in the chapter Registration of frontend plugins.

There are basically two ways to create frontend plugins in TYPO3:

  1. With the Extbase framework using configurePlugin() in the file ext_localconf.php and registerPlugin() in the file Configuration/TCA/Overrides/tt_content.php
  2. Create a plugin using Core functionality (without Extbase) and a custom controller

Generally speaking, if you already use Extbase, it is good practice to create your plugins using the Extbase framework. This also involves:

  • creating controller actions
  • create a domain model and repository (if your plugin requires records that are persisted in the database)
  • create a view using Fluid templates

Configure custom backend preview for content element 

To allow editors a smoother experience, all custom content elements and plugins should be configured with a corresponding backend preview that shows an approximation of the element's appearance in the TYPO3 page module. The following sections describe how to achieve that.

A preview renderer is used to facilitate (record) previews in TYPO3. This class is responsible for generating the preview and the wrapping.

The default preview renderer is \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer and handles the Core's built-in content types (field CType in table tt_content).

Extend the default preview renderer 

There are two ways to provide previews for your custom content types: via page TSconfig or event listener.

Page TSconfig 

This is the "integrator" way, no PHP coding is required. Just some page TSconfig and a Fluid template.

EXT:my_extension/Configuration/page.tsconfig
mod.web_layout {
  tt_content {
    preview {
      # Your CType
      example_ctype = EXT:my_extension/Resources/Private/Templates/Preview/ExampleCType.html
    }
  }
}
Copied!

In the Fluid template, the following variables are available:

  • All properties of the tt_content row (for example {uid}, {title}, and {header})
  • The current record as object ( \TYPO3\CMS\Core\Domain\Record ) in {record}
  • FlexForm settings as array in {pi_flexform_transformed}

For more details see the TSconfig Reference.

Event listener 

This requires at least some PHP coding, but allows more flexibility in accessing and processing the content elements properties.

New in version 12.0

Since version 12.0 this technique replaces the former hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']

The event PageContentPreviewRenderingEvent is being dispatched by the StandardContentPreviewRenderer. You can listen to it with your own event listener.

Have a look at this showcase implementation.

For general information see the chapter on implementing an event listener.

Writing a preview renderer 

A custom preview renderer must implement the interface \TYPO3\CMS\Backend\Preview\PreviewRendererInterface which contains the following API methods:

interface PreviewRendererInterface
Fully qualified name
\TYPO3\CMS\Backend\Preview\PreviewRendererInterface

Interface PreviewRendererInterface

Contract for classes capable of rendering previews of a given record from a table. Responsible for rendering preview header, preview content and wrapping of those two values.

Responsibilities are segmented into three methods, one for each responsibility, which is done in order to allow overriding classes to change those parts individually without having to replace other parts. Rather than relying on implementations to be friendly and divide code into smaller pieces and give them (at least) protected visibility, the key methods are instead required on the interface directly.

Callers are then responsible for calling each method and combining/wrapping the output appropriately.

renderPageModulePreviewHeader ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for rendering preview header HTML for the page module only. Receives the GridColumnItem that contains the record for which a preview header should be rendered and returned.

param $item

the item

Returns
string
renderPageModulePreviewContent ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for rendering preview body HTML for the page module only. Receives the GridColumnItem that contains the record for which a preview should be rendered and returned.

param $item

the item

Returns
string
renderPageModulePreviewFooter ( \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Render a footer for the record to display in page module below the body of the item's preview.

param $item

the item

Returns
string
wrapPageModulePreview ( string $previewHeader, string $previewContent, \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem $item)

Dedicated method for wrapping a preview header and body HTML. Receives $item, an instance of GridColumnItem holding among other things the record, which can be used to determine appropriate wrapping.

param $previewHeader

the previewHeader

param $previewContent

the previewContent

param $item

the item

Returns
string

Implementing these methods allows you to control the exact composition of the preview.

This means assuming your preview renderer returns <h4>Header</h4> from the header render method and <p>Body</p> from the preview content rendering method and your wrapping method does return '<div>' . $previewHeader . $previewContent . '</div>'; then the entire output becomes <div><h4>Header</h4><p>Body</p></div> when combined.

Should you wish to reuse parts of the default preview rendering and only change, for example, the method that renders the preview body content, you can extend \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer in your custom preview renderer class - and selectively override the methods from the API displayed above.

Configuring the implementation 

Individual preview renderers can be defined by using one of the following approaches:

  1. Any record

    $GLOBALS['TCA'][$table]['ctrl']['previewRenderer']
        = MyVendor\MyExtension\Preview\MyPreviewRenderer::class;
    Copied!

    This specifies the preview renderer to be used for any record in $table.

  2. Table has a type field/attribute

    $GLOBALS['TCA'][$table]['types'][$type]['previewRenderer']
        = MyVendor\MyExtension\Preview\MyPreviewRenderer::class;
    Copied!

    This specifies the preview renderer only for records of type $type as determined by the type field of your table.

Deprecated since version 13.4

New content element wizard 

Changed in version 13.0

Custom content element types are auto-registered for the New Content Element wizard. The listing can be configured using TCA.

The content element wizard opens when a new content element is created. It can be fully configured using ref:Page TSconfig <t3tsref:pagetsconfig>.

The wizard looks like this:

  1. The title can be a string or, recommended, a language reference.
  2. The description can be a string or, recommended, a language reference.
  3. The group can be one of the existing group identifiers or a new one.
  4. The icon can be one of the existing registered icon keys or a custom icon key registered in the icon API.

Any of these entries can be omitted. You should at least define a title.

New content elements are usually added in extensions in file EXT:my_extension/Configuration/Overrides/tt_content.php.

The following groups are available by default:

default

Changed in version 13.0 This group was renamed from group `common`.

Default group for commonly used content elements

forms
Content elements representing forms like a contact form or a login form

lists

menu
Menus that can be inserted as content elements like a sitemap or a menu of all subpages.
plugins
Plugins provided by extensions
special
Content elements that are used of special cases

All content element groups are listed in $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups'] you can debug them in the TYPO3 backend using the backend module System > Configuration if typo3/cms-lowlevel is installed and you are an administrator.

Some third party extensions like bk2k/bootstrap-package are altering the available groups.

Plain content elements or plugins 

You can add a content element or plain plugin (no Extbase) using method ExtensionManagementUtility::addPlugin(): of class \TYPO3\CMS\Core\Utility\ExtensionManagementUtility .

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addPlugin(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
        'value' => 'myextension_myplugin',
        'icon' => 'content-text',
        'group' => 'plugin',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
    ],
    'CType',
    'my_extension',
);
Copied!

The key value in the parameter $itemArray is used as key of the newly added content element representing the plugin.

When you are using CType for parameter $type the content element is added to the select item list of column CType in table tt_content.

Deprecated since version 13.4

Using the default list_type for the parameter is deprecated. All content elements and plugins should be added with string CType for parameter $type.

This method supplies some default values:

group
Defaults to default
icon
Icon of the extension if defined

While it is still possible to use ExtensionManagementUtility::addTcaSelectItem() as is commonly seen in older extensions this method is not specific to content elements and therefore sets not default values for group and icon.

Plugins (Extbase) in the "New Content Element" wizard 

To add an Extbase plugin you can use ExtensionManagementUtility::registerPlugin of class \TYPO3\CMS\Extbase\Utility\ExtensionManagementUtility.

This method is only available for Extbase plugins defined via ExtensionUtility::configurePlugin in file EXT:my_extension/ext_localconf.php

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionUtility::registerPlugin(
    'my_extension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
    'myextension_myplugin',
    'plugins',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
);
Copied!

Override the wizard with page TSconfig 

The TCA is always set globally for the complete TYPO3 installation. If you have a multi-site installation and want to alter the appearance of content elements in the wizard or remove certain content elements this can be done via page TSconfig. This is commonly done on a per site basis so you can use the Site set page TSconfig provider in your site package.

You can use the settings of newContentElement.wizardItems.

Remove items from the "New Content Element" wizard 

Using [group].removeItems you can remove a content element type from the wizard.

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  special.removeItems := addToList(html)
}
Copied!

This removes the content element "Plain HTML" from the group special.

You can also remove whole groups of content elements from the wizard:

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
# This will remove the "menu" group
mod.wizards.newContentElement.wizardItems.removeItems := addToList(menu)
Copied!

Change title, description, icon and default values in the wizard 

You can use the following page tsconfig properties to change the display of the element in the wizard:

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  special.html {
    title = Plain HTML(use with care!!)
    description (
      Attention: This HTML is output unsanized! The editor is responsible
      to only use safe HTML content.
    )
    icon = mysitepackage_htmlattention
  }
}
Copied!

Register a new group in the "New Content Element" wizard 

New groups are added on the fly, however it is recommended to set a localized header:

EXT:my_extension/Configuration/Overrides/tt_content.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

defined('TYPO3') or die();

ExtensionManagementUtility::addTcaSelectItemGroup(
    'tt_content',
    'CType',
    'myextension_myplugingroup',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin.group',
);

ExtensionUtility::registerPlugin(
    'my_extension',
    'MyPlugin',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_title',
    'myextension_myplugin',
    'myextension_myplugingroup',
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_myplugin_description',
);
Copied!

The headers can also be overrriden on a per site basis using page TSconfig.

EXT:my_sitepackage/Configuration/Sets/MySet/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  mygroup.header = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:mygroup.header
}
Copied!

Content elements compatible with TYPO3 v12.4 and v13 

If your extension supplies content elements or plugins and supports both TYPO3 v12.4 and v13 you can keep the Page TsConfig for the New Content Element Wizard while you additionally supply the TCA settings for TYPO3 v13.

You should use the same content element group for both definition ways or the content element will be displayed twice, once in each group. Group common is automatically migrated to default for TYPO3 v13.

Best practices 

Following are some good practices for creating custom content element types and plugins and for customizing content elements for usage in the backend.

Coding / structure 

  • Use a sitepackage extension to maintain your site customization (such as backend layouts, custom content elements etc.)
  • How you structure your extensions depends a little on the use case and if they will be reused in several projects and / or made public. If you create one extension for every custom content element, you may want to think about whether they might be merged into one extension.
  • Do not use deprecated functionality. Read the Core Changelog to check for deprecations and breaking changes between TYPO3 versions.
  • Some naming conventions are described in the chapter Naming conventions.
  • Read (or skim) the Coding guidelines.

Backend usability 

  • Make it easier for your editors by hiding the following by configuration

    • content elements that should not be used in the "Content Element Wizard"
    • fields that should not be filled out in the backend forms.

Migration: list_type plugins to CType 

Deprecated since version 13.4

The plugin content element ( list) and the plugin sub types field ( list_type) have been marked as deprecated in TYPO3 v13.4 and will be removed in TYPO3 v14.0.

Several steps are important in the migration from list_type plugins to CType plugins:

  • Register plugins using the CType record type
  • Create update wizard which extends \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate and add list_type to CType mapping for each plugin to migrate.
  • Migrate possible FlexForm registration and add dedicated showitem TCA configuration
  • Migrate possible PreviewRenderer registration in TCA
  • Adapt possible content element wizard items in page TSconfig, where list_type is used
  • Adapt possible content element restrictions in backend layouts or container elements defined by third-party extensions like EXT:content_defender

Migration example: Extbase plugin 

1. Adjust the Extbase plugin configuration 

Extbase plugins are usually registered using the utility method \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() in file EXT:my_extension/ext_localconf.php.

Add value ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT as fifth parameter, $pluginType, to the method ExtensionUtility::configurePlugin():

EXT:examples/ext_localconf.php (difference)
 use T3docs\Examples\Controller\FalExampleController;
 use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

 ExtensionUtility::configurePlugin(
     'Examples',
     'HtmlParser',
     [
         HtmlParserController::class => 'index',
     ],
+    [],
+    ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT,
 );
Copied!

If the fourth parameter, $nonCacheableControllerActions was missing you can set it to an empty array, the default value.

It is theoretically possible that the extension author did not use this utility method. In that case you have to change the TypoScript used to display your plugin manually. This step is similar to adjusting the TypoScript of a Core-based plugin.

2. Adjust the registration of FlexForms and additional fields 

EXT:examples/Configuration/TCA/Overrides/tt_content.php (difference)
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

 $extensionKey = 'Examples';
 $pluginName = 'HtmlParser';
 $pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang.xlf:htmlparser_plugin_title';

 // Register the HTML Parser plugin
 $pluginSignature = ExtensionUtility::registerPlugin(
     $extensionKey,
     $pluginName,
     $pluginTitle,
 );

-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]
-    = 'layout,select_key,pages';
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature]
-    = 'pi_flexform';
-
-ExtensionManagementUtility::addPiFlexFormValue(
-    $pluginSignature,
-    'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',
-);

+ExtensionManagementUtility::addToAllTCAtypes(
+    'tt_content',
+    '--div--;Configuration,pi_flexform,',
+    $pluginSignature,
+    'after:subheader',
+);
+
+ExtensionManagementUtility::addPiFlexFormValue(
+    '*',
+    'FILE:EXT:examples/Configuration/Flexforms/HtmlParser.xml',
+    $pluginSignature,
+);
Copied!

The CType based plugin does not inherit the default fields provided by the TCA of the content element "List". These where in many cases removed by using subtypes_excludelist.

As these fields are not displayed automatically anymore you can remove this definition without replacement: Line 15 in the diff. If they have not been removed and are still needed, you will need to manually add them to your plugin type.

The subtypes_addlist was used to display the field containing the FlexForm, an possibly other fields in the list_type plugin. We remove this definition (Line 17) and replace it by using the utility method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes() (Line 25-30).

The utility method ExtensionManagementUtility::addPiFlexFormValue() needs to be changed from using the first parameter for the $pluginSignature to using the third parameter. The first parameter requires a certain list_type setting it to the wildcard * allows all list types. The third parameter limits it to the CType.

3. Provide an upgrade wizard 

New in version 13.4

You can extend class AbstractListTypeToCTypeUpdate to provide a custom upgrade wizard that moves existing plugins from the list_type definition to the CType definition. The resulting upgrade wizard will even adjust backend user permissions for the defined plugins:

Class T3docs\Examples\Upgrades\ExtbasePluginListTypeToCTypeUpdate
final class ExtbasePluginListTypeToCTypeUpdate extends AbstractListTypeToCTypeUpdate
{

}
Copied!

4. Search your code and replace any mentioning of list_type 

Search your code. If you used the list_type of your plugin in any custom database statement or referred to the according key within TypoScript, you will need to do the possible proper replacement.

Search your TCA definitions for any use of the now outdated configuration options.

Note, that if your old key was something like "example_pi1" you are not forced to set the CType to "ExamplePi1" with UpperCamelCase, but you could keep the old identifier. This makes replacements less time consuming.

Migration example: Core-based plugin 

1. Adjust the plugin registration 

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php (diff)
 $pluginSignature = 'examples_pi1';
 $pluginTitle = 'LLL:EXT:examples/Resources/Private/Language/locallang_db.xlf:tt_content.examples_pi1.title';
 $extensionKey = 'examples';
 $flexFormPath = 'FILE:EXT:examples/Configuration/Flexforms/flexform_ds1.xml';

 // Add the plugins to the list of plugins
 ExtensionManagementUtility::addPlugin(
     [$pluginTitle, $pluginSignature, '', 'plugin'],
-    'list_type',
+    'CType',
     $extensionKey,
 );

-// Disable the display of layout and select_key fields for the plugin
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist'][$pluginSignature]
-    = 'layout,select_key,pages';
-
-// Activate the display of the plug-in flexform field and set FlexForm definition
-$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature] = 'pi_flexform';

+// Activate the display of the FlexForm field
+ExtensionManagementUtility::addToAllTCAtypes(
+    'tt_content',
+    '--div--;Configuration,pi_flexform,',
+    $pluginSignature,
+    'after:subheader',
+);

 ExtensionManagementUtility::addPiFlexFormValue(
-    $pluginSignature,
+    '*',
     $flexFormPath,
+    $pluginSignature,
 );
Copied!

2. Adjust the TypoScript of the plugin 

If your plugin was rendered using typo3/cms-fluid-styled-content you are probably using the top level TypoScript object tt_content to render the plugin. The path to the plugin rendering needs to be adjusted as you cannot use the deprecated content element "list" anymore:

EXT:my_extension/Configuration/Sets/MyPluginSet/setup.typoscript (diff)
-tt_content.list.20.examples_pi1 = USER
-tt_content.list.20.examples_pi1 {
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1 {
     20 = USER
     20 {
         userFunc = MyVendor\Examples\Controller\ExampleController->example
         settings {
             singlePid = 42
             listPid = 55
         }
         view {
             templateRootPaths.10 = {$templateRootPath}
             partialRootPaths.10 = {$partialRootPath}
             layoutRootPaths.10 = {$layoutRootPath}
         }
     }
     templateName = Generic
 }

 # Or if you used the plugin top level object:

-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
Copied!

3. Provide an upgrade wizard for automatic content migration for TYPO3 v13.4 and v12.4 

If you extension only support TYPO3 v13 and above you can extend the Core class \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate .

If your extension also supports TYPO3 v12 and maybe even TYPO3 v11 you can use class \Linawolf\ListTypeMigration\Upgrades\AbstractListTypeToCTypeUpdate instead. Require via composer: linawolf/list-type-migration or copy the file into your extension using your own namespaces:

EXT:my_extension/Classes/Upgrades/PluginListTypeToCTypeUpdate.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Upgrades;

// composer req linawolf/list-type-migration
use Linawolf\ListTypeMigration\Upgrades\AbstractListTypeToCTypeUpdate;
use TYPO3\CMS\Install\Attribute\UpgradeWizard;

#[UpgradeWizard('myExtensionPluginListTypeToCTypeUpdate')]
final class PluginListTypeToCTypeUpdate extends AbstractListTypeToCTypeUpdate
{
    protected function getListTypeToCTypeMapping(): array
    {
        return [
            'my_extension_pi1' => 'my_extension_pi1',
            'my_extension_pi2' => 'my_extension_newpluginname',
        ];
    }

    public function getTitle(): string
    {
        return 'Migrates my_extension plugins';
    }

    public function getDescription(): string
    {
        return 'Migrates my_extension_pi1, my_extension_pi2 from list_type to CType.';
    }
}
Copied!

If you also have to be compatible with TYPO3 v11, register the upgrade wizard manually: Registering wizards for TYPO3 v11.

Content Security Policy 

New in version 12.3

Introduction 

Content Security Policy (CSP) is a security standard introduced to prevent cross-site scripting (XSS), clickjacking and other code injection attacks resulting of malicious content being executed in the trusted web page context.

Think of CSP in terms of an "allow/deny" list for remote contents.

CSP rules are used to describe, which external assets or functionality are allowed for certain HTML tags (like <script>, <img>, <iframe>). This allows to restrict external resources or JavaScript execution with security in mind. When accessing a page, these rules are sent as part of the HTTP request from the server to the browser, and the browser will enforce these rules (and reject non-allowed content). These rejection are always logged in the browser console. Additionally, external tools can be configured to receive and track violations of the policy.

Content Security Policy declarations can be applied to a TYPO3 website in frontend and backend scope with a dedicated API. This API allows for site-specific or extension-specific configuration instead of manually setting the CSP rules with server-side configuration through httpd.conf/nginx.conf or .htaccess files.

To delegate Content Security Policy handling to the TYPO3 frontend, at least one of the feature flags:

needs to be enabled, or the site-specific csp.yaml configuration file needs to set the enforce or report disposition like this:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  # site-specific mutations could also be listed, like this:
  # mutations:
  #  - mode: "append"
  #    directive: "img-src"
  #    sources:
  #      - "cdn.example.com"
  #      - "assets.example.com"

# alternatively (or additionally!), reporting can be set too,
# for example when testing stricter rules than above
# (note the missing 'assets.example.com')
# report:
#   inheritDefault: true
#   mutations:
#    - mode: "append"
#      directive: "img-src"
#      sources:
#        - "cdn.example.com"
#
Copied!

Changed in version 13.0

In the TYPO3 backend the Content Security Policy is always enforced.

Within the TYPO3 backend, a specific backend module is available to inspect policy violations / reports, and there is also a list to see all configured CSP rules, see section Active content security policy rules.

Terminology 

This document will use very specific wording that is part of the CSP W3C specification, these terms are not "invented" by TYPO3. Since reading the W3C RFC can be very intimidating, here are a few key concepts.

Directives 

  • CSP consists of multiple rules (or "directives"), that are part of a "policy". This policy says, what functionality the site's output is allowed to use.
  • With that, several HTML tags can be controlled, like from which URLs images can be requested from, if and from where iframes are allowed, if and from where JavaScripts are allowed and so on. There is a long list of applicable directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives with specific identifiers like default-src, img-src and so on.
  • Each directive may have several "attributes" (like the allowed URLs).
  • Directives may build upon each other, a bit like CSS definitions (Cascading Style Sheet) do. However, these are more meant to modify a basic rule on an earlier level, and called "mutations". The relation of a "child rule" to its "parent" is also called "ancestor chain". An example:

    If frame-src is not defined, it falls back to child-src, and finally falls back to default-src. But if frame-src is defined, it is used, and the sources from default-src are not used. In such a case, default-src listed sources have to be repeated (when wanted) explicitly in frame-src.

Applying the policy 

  • A final policy is compiled of all these directives, and then sent as a HTTP response header Content-Security-Policy: ... (respectively Content-Security-Policy-Reporty-Only).
  • In TYPO3, directives can be specified via PHP syntax (within Extensions) and YAML syntax (within site configuration). Additionally, rules can be set via the PSR-14 event PolicyMutatedEvent.

Mutations 

  • These rules can influence each other, this is where the concept of "mutations" come in. The "policy builder" of TYPO3 applies each configured mutation, no matter where it was defined.
  • Because of this, each mutation (directive definition) needs a specific "mode" that can instruct, how this mutation is applied: Should an existing directive be set, inherited, appended, remove or extended to the final policy (see Content Security Police modes).
  • Each directive is then applied in regard to its defined mode and can list one or more "sources" with the values of additional parameters of a directive. Sources are web site addresses / URLs (or just protocols), and also include some special keywords like self/none/data:.

Nonces 

  • There are possible exemptions to directives for specific content created on specific pages created by TYPO3 in your frontend (or backend modules). To verify, that these exemptions are valid in a policy, a so-called "Nonce" (a unique "number used once") is created (details on https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce).

    • TYPO3 can manage these Nonces and apply them were configured.
    • Nonces are retrieved from \TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce and will be used for any directive within the scope of a single HTTP request.
    • More details are covered in Nonce.

Policy violations and reporting 

  • When a webpage with activated policies is shown in a client's browser, each HTML tag violating the policy will not be interpreted by the browser.
  • Depending on a configuration of a possible "Report", such violations can be submitted back to a server and be evaluated there. TYPO3 provides such an endpoint to receive and display reports in a backend module, but also third-party servers are usable.
  • Policies can be declared with "dispositions", to indicate how they are handled. "Enforce" means that a policy is in effect, and "Report" allows to only pretend a policy is in effect, to gather knowledge about possible improvements of a webpage's output. Both dispositions can be set independently in TYPO3.
  • All active rules can be seen in the backend configuration section, see Active content security policy rules.

Example scenario 

Let's define a small real-world scenario:

  • You have one TYPO3 installation with two sites (frontend), example.com and example.org.
  • You have created custom backend modules for some distinct functionality.
  • example.com is a site where your editors fully control all frontend content, and they want to have full flexibility of what and how to embed. You use a CDN network to deliver your own large assets, like videos.
  • example.org is a community-driven site, where users can manage profiles and post chats, and where you want to prevent exploits on your site. Some embedding to a set of allowed web services (YouTube, Google Analytics) must be possible.

So you need to take care of security measures, and find a pragmatic way how to allow foreign content (like YouTube, widgets, tracking codes, assets)

Specifically you want to to set these following rules, as an example.

Rules for example.com (editorial) 

  • <iframe> to many services should be allowed
  • <img> sources to anywhere should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for example.org (community) 

  • <iframe> to cdn.example.com, *.youtube.com should be allowed
  • <img> sources to cdn.example.com and *.instagram.com should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for the TYPO3 backend 

Normal TYPO3 backend rules need to be applied, so we only want to add some rules for custom backend modules:

  • <iframe> to cdn.example.com should be allowed
  • <img> sources to cdn.example.com should be allowed
  • <script> sources to cdn.example.com should be allowed

Resulting configuration example: 

And this is how you would do that with a CSP YAML configuration file, one per site:

config/sites/example-com/csp.yaml | typo3conf/sites/example-com/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frames/iframes to TRUSTED specific locations
  # Avoid "protocol only" white-list like "https:" here,
  # because it could inject javascript easily, the most important reason
  # why CSP was invented was to block security issues like this.
  # (Note: it's "frame-src" not "iframe-src")
  - mode: "extend"
    directive: "frame-src"
    sources:
      - "https://*.example.org"
      - "https://*.example.com"
      - "https://*.instagram.com"
      - "https://*.vimeo.com"
      - "https://*.youtube.com"

  # Allow img src to anyhwere (HTTPS only, not HTTP)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https:"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
config/sites/example-org/csp.yaml | typo3conf/sites/example-org/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frame/iframe src to the specified domains (HTTPS only)
  - mode: "extend"
    # (Note: it's "frame-src" not "iframe-src")
    directive: "frame-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"

  # Allow img src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.instagram.com"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    // Provide declarations for the backend only
    Scope::backend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Extend,
            // Note: it's "FrameSrc" not "IFrameSrc"
            Directive::FrameSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ScriptSrc,
            new UriValue('https://cdn.example.com'),
        ),
    ),
]);
Copied!

This is really just a simple demo, that has room for improvements. For example, the allowed list of *-src values to any directive could actually be set through their common parent, the default-src attribute. There is a very deep and nested possibility to address the attributes of many HTML5 tags, which is covered in depth on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives.

You can take a look into the PHP enum \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive , which gives you an overview of all supported directives.

Read on to understand more of the underlying API builder concepts below.

Configuration 

Policy builder approach 

The following approach illustrates how a policy is build:

<?php

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Security\Nonce;

$nonce = Nonce::create();
$policy = (new Policy())
    // Results in `default-src 'self'`
    ->default(SourceKeyword::self)

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `img-src 'self' data: https://*.typo3.org`
    ->extend(Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org'))

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `script-src 'self' 'nonce-[random]'`
    // ('nonce-proxy' is substituted when compiling the policy)
    ->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)

    // Sets (overrides) the directive,
    // thus ignores 'self' of the 'default-src' directive
    // Results in `worker-src blob:`
    ->set(Directive::WorkerSrc, SourceScheme::blob);

header('Content-Security-Policy: ' . $policy->compile($nonce));
Copied!

The result of the compiled and serialized result as HTTP header would look similar to this (the following sections are using the same example, but utilize different techniques for the declarations):

Content-Security-Policy: default-src 'self';
    img-src 'self' data: https://*.typo3.org; script-src 'self' 'nonce-[random]';
    worker-src blob:
Copied!

Extension-specific 

Policies for frontend and backend can be applied automatically by providing a Configuration/ContentSecurityPolicies.php file in an extension, for example:

EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries(
    [
        // Provide declarations for the backend
        Scope::backend(),
        // NOTICE: When using `MutationMode::Set` existing declarations will be overridden

        new MutationCollection(
            // Results in `default-src 'self'`
            new Mutation(
                MutationMode::Set,
                Directive::DefaultSrc,
                SourceKeyword::self,
            ),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `img-src 'self' data: https://*.typo3.org`
            new Mutation(
                MutationMode::Extend,
                Directive::ImgSrc,
                SourceScheme::data,
                new UriValue('https://*.typo3.org'),
            ),
            // NOTICE: the following two instructions for `Directive::ImgSrc` are identical to the previous instruction,
            // `MutationMode::Extend` is a shortcut for `MutationMode::InheritOnce` and `MutationMode::Append`
            // new Mutation(MutationMode::InheritOnce, Directive::ImgSrc, SourceScheme::data),
            // new Mutation(MutationMode::Append, Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org')),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `script-src 'self' 'nonce-[random]'`
            // ('nonce-proxy' is substituted when compiling the policy)
            new Mutation(
                MutationMode::Extend,
                Directive::ScriptSrc,
                SourceKeyword::nonceProxy,
            ),

            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src blob:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                SourceScheme::blob,
            ),
        ),
    ],
    [
        // You can also additionally provide frontend declarations
        Scope::frontend(),
        new MutationCollection(
            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src https://*.workers.example.com:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                new UriValue('https://*.workers.example.com'),
            ),
        ),
    ],
);
Copied!

The API here is much like the YAML syntax. The PHP code needs to return a mapped array of an MutationCollection instance with all rules put into a sub-array, containing instances of a single Mutation.

Each Mutation instance is like a Data Object (DO) where its constructor allows you to specifiy a mode (type MutationMode), a directive (type Directive) and one ore more actual values ("sources", type UriValue or SourceKeyword).

Additionally, a Scope instance object is included, which can either be Scope::backend() or Scope::frontend().

A good PHP IDE will allow for good autocompletion and hinting, and using a boilerplate configuration like the example above helps you to get started.

Backend-specific 

The YAML configuration only applies to the frontend part of TYPO3. Backend policies need to be set using the PHP API, within an extension as described in the section above.

You need to ensure that Scope::backend() is set in the mapped return array for the rules you want to setup.

Site-specific (frontend) 

In frontend, a dedicated sites/<my_site>/csp.yaml can be used to declare policies for a specific site, for example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# Inherits default site-unspecific frontend policy mutations (enabled per default)
inheritDefault: true
mutations:
  # Results in `default-src 'self'`
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `img-src 'self' data: https://*.typo3.org`
  - mode: "extend"
    directive: "img-src"
    sources:
      - "data:"
      - "https://*.typo3.org"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `script-src 'self' 'nonce-[random]'`
  # ('nonce-proxy' is substituted when compiling the policy)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "'nonce-proxy'"

  # Results in `worker-src blob:`
  - mode: "set"
    directive: "worker-src"
    sources:
      - "blob:"
Copied!

Disable CSP for a site 

The Content Security Policy for a particular site can be disabled with the active key set to false:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# "active" is enabled by default if omitted
active: false
Copied!

Site-specific Content-Security-Policy endpoints 

The reporting endpoint is used to receive browser reports about violations to the security policy, for example if a YouTube URL was requested, but could not be displayed in an iframe due to a directive not allowing this.

Reports like this can help to gain insight, what URLs are used by editors and might need inclusion into the policy.

Since reports can be sent by any browser, they can possibly easily flood a site with requests and take up storage space. Reports are stored in the sys_http_report database table when using the endpoint provided by TYPO3.

To influence whether this endpoint accepts reports, the disposition-specific property reportingUrl can be configured and set to either:

true
to enable the reporting endpoint
false
to disable the reporting endpoint
(string)
to use the given value as external reporting endpoint

If defined, the site-specific configuration takes precedence over the global configuration contentSecurityPolicyReportingUrl.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

Changed in version 12.4.27 / 13.4.5

Example: Disabling the reporting endpoint 
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: false
Copied!
Example: Using custom external reporting endpoint 
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: https://example.org/csp-report
Copied!

Content Security Police modes 

Adjusting specific directives / mutations for a policy can be performed via the following modes:

append

append
YAML
append
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Append

Appends to a given directive.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "example.org"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            new UriValue('example.org'),
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src example.org example.com
Copied!

extend

extend
YAML
extend
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Extend

Extends the given directive. It is a shortcut for inherit-once and append.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "extend"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src 'self' example.com
Copied!

inherit-again

inherit-again
YAML
inherit-again
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritAgain

Inherits again from the corresponding ancestor chain and merges existing sources.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-again"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-again"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ScriptSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src data: 'self' example.com
Copied!

Note that data: is inherited to img-src (in opposite to inherit-once).

inherit-once

inherit-once
YAML
inherit-once
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritOnce

Inherits once from the corresponding ancestor chain. When inherit-once is called multiple times on the same directive, only the first time is applied.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-once"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-once"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src 'self' example.com
Copied!

Note that data: is not inherited to img-src. If you want to inherit also data: to img-src use inherit-again.

reduce

reduce
YAML
reduce
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Reduce

Reduces a directive by a given aspect.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
      - "data:"
      - "example.com"

  - mode: "reduce"
    directive: "img-src"
    sources:
      - "data:"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
            SourceScheme::data,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Reduce,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self' example.com
Copied!

remove

remove
YAML
remove
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Remove

Removes a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "data:"

  - mode: "remove"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::Remove,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'
Copied!

set

set
YAML
set
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Set

Sets (overrides) a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: img-src 'self'
Copied!

Nonce 

The nonce attribute is useful to allowlist specific elements, such as a particular inline script or style elements. It can help you to avoid using the CSP unsafe-inline directive, which would allowlist all inline scripts or styles.

-- MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce

It may look like this in your HTML code:

<link
    rel="stylesheet"
    href="/_assets/af46f1853e4e259cbb8ebcb816eb0403/Css/styles.css?1687696548"
    media="all"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
>

<style nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline styles */
</style>

<script
    src="/_assets/27334a649e36d0032b969fa8830590c2/JavaScript/scripts.js?1684880443"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
></script>

<script nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline JavaScript */
</script>
Copied!

The nonce changes with each request so that (possibly malicious) inline scripts or styles are blocked by the browser.

The nonce is applied automatically, when scripts or styles are defined with the TYPO3 API, like TypoScript ( page.includeJS, etc.) or the asset collector. This only refers to referenced files (via src and href attributes) and not inline scripts or inline styles. For those, you should either use the PHP/Fluid approach as listed below, or use TypoScript only for passing DOM attributes and using external scripts to actually evaluate these attributes to control functionality.

TYPO3 provides APIs to get the nonce for the current request:

Retrieve with PHP 

The nonce can be retrieved via the nonce request attribute:

// use TYPO3\CMS\Core\Domain\ConsumableString

/** @var ConsumableString|null $nonceAttribute */
$nonceAttribute = $this->request->getAttribute('nonce');
if ($nonceAttribute instanceof ConsumableString) {
    $nonce = $nonceAttribute->consume();
}
Copied!

In a Fluid template 

The f:security.nonce ViewHelper is available, which provides the nonce in a Fluid template, for example:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<script nonce="{f:security.nonce()}">
    const inline = 'script';
</script>

<style nonce="{f:security.nonce()}">
    .some-style { color: red; }
</style>
Copied!

You can also use the f:asset.script or f:asset.css ViewHelpers with the useNonce attribute:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:asset.script identifier="my-inline-script" useNonce="1">
    const inline = 'script';
</f:asset.script>

<f:asset.css identifier="my-inline-style" useNonce="1">
    .some-style { color: red; }
</f:asset.css>
Copied!

Notes about nonces and caching 

Nonces are implemented via a PSR middleware and thus applied dynamically. This also means, they are somewhat "bad" for caching (especially for reverse proxies), since they create unique output for a specific visitor.

Since the goal of nonces are to allow "exemptions" for otherwise forbidden content, this closely relates to validity or integrity of this forbidden content. Instead of emitting unique nonces, another possibility is to utilize hashing functionality to content regarded as "safe".

This can be done with sha256/sha384/sha512 hashing of referenced script, and including them as a valid directive, like this:

The "sha256-..." block would be the SHA256 hash created from a file like 'script.js'.

For example, a file like this:

script.js (some javascript file that is included in your website)
console.log('Hello.');
Copied!

would correspond to a SHA256 hash of 6c7d3c1bf856597a2c8ae2ca7498cb4454a32286670b20cf36202fa578b491a9.

The browser would evaluate a reference JavaScript file and calculate it's SHA256 hash and compare it to the list of allowed hashes.

The downside of this is: Everytime an embedded file changes (like via build processes), the CSP SHA hash would need to be adopted. This could be automated by a PHP definition of CSP rules and hashing files automatically, which would be a performance-intense process and call for its own caching.

There is no automatism for this kind of hashing in TYPO3 (yet, see https://forge.typo3.org/issues/100887), so it has to be done manually as outlined above.

Reporting of violations, CSP Backend module 

Potential CSP violations are reported back to the TYPO3 system and persisted internally in the database table sys_http_report. A corresponding Admin Tools > Content Security Policy backend module supports users to keep track of recent violations and - if applicable - to select potential resolutions (stored in the database table sys_csp_resolution) which extends the Content Security Policy for the given scope during runtime:

Backend module "Content Security Policy" which displays the violations

Clicking on a row displays the details of this violation on the right side including suggestions on how to resolve this violation. You have the choice to apply this suggestion, or to mute or delete the specific violation.

Using a third-party service 

As an alternative to the built-in reporting module, an external reporting URL can be configured to use a third-party service as well:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';
Copied!

Violations are then sent to the third-party service instead of the TYPO3 endpoint. Resolutions would then not be applied dynamically.

Disabling content security policy reporting globally 

Administrators can disable the reporting endpoint globally or configure it per site as needed. (See Example: Disabling the reporting endpoint).

If defined, the site-specific configuration takes precedence over the global configuration.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

The global scope-specific setting contentSecurityPolicyReportingUrl can be set to zero ('0') to disable the CSP reporting endpoint:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!

Active content security policy rules 

The backend module System > Configuration > Content Security Policy Mutations uses a simple tree display of all configured directives, grouped by frontend or backend. Each rule shows where it is defined, and what its final policy is set to:

Backend module "Configuration > Content Security Policy Mutations" which displays a tree of all policy directives.

PSR-14 events 

The following PSR-14 events are available:

Context API and aspects 

Introduction 

The Context API encapsulates various information for data retrieval (for example, inside the database) and analysis of current permissions and caching information.

The context is set up at the very beginning of each TYPO3 entry point, keeping track of, for example, the current time, if a user is logged in and which workspace is currently accessed.

The \TYPO3\CMS\Core\Context\Context object can be retrieved via dependency injection:

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}
}
Copied!

This information is separated in so-called "aspects", each being responsible for a certain area.

Aspects 

Date time aspect 

Contains time, date and timezone information for the current request.

The date time aspect, \TYPO3\CMS\Core\Context\DateTimeAspect , accepts the following properties:

timestamp

timestamp
Call
$this->context->getPropertyFromAspect('date', 'timestamp');

Returns the Unix timestamp as an integer value.

timezone

timezone
Call
$this->context->getPropertyFromAspect('date', 'timezone');

Returns the timezone name, for example, "Germany/Berlin".

iso

iso
Call
$this->context->getPropertyFromAspect('date', 'iso');

Returns the datetime as string in ISO 8601 format, for example, "2004-02-12T15:19:21+00:00".

full

full
Call
$this->context->getPropertyFromAspect('date', 'full');

Returns the complete \DateTimeImmutable object.

Example 

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $currentTimestamp = $this->context->getPropertyFromAspect(
            'date',
            'timestamp',
        );

        // ... do something with $currentTimestamp
    }
}
Copied!

Language aspect 

Contains information about language settings for the current request, including fallback and overlay logic.

The language aspect, \TYPO3\CMS\Core\Context\LanguageAspect accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('language', 'id');

Returns the requested language of the current page as integer (uid).

contentId

contentId
Call
$this->context->getPropertyFromAspect('language', 'contentId');

Returns the language ID of records to be fetched in translation scenarios as integer (uid).

fallbackChain

fallbackChain
Call
$this->context->getPropertyFromAspect('language', 'fallbackChain');

Returns the fallback steps as array.

overlayType

overlayType
Call
$this->context->getPropertyFromAspect('language', 'overlayType');

Returns one of

  • LanguageAspect::OVERLAYS_OFF
  • LanguageAspect::OVERLAYS_MIXED
  • LanguageAspect::OVERLAYS_ON or
  • LanguageAspect::OVERLAYS_ON_WITH_FLOATING (default)

See Overlay types for more details.

legacyLanguageMode

legacyLanguageMode
Call
$this->context->getPropertyFromAspect('language', 'legacyLanguageMode');

Returns one of

  • strict
  • ignore or
  • content_fallback.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

legacyOverlayType

legacyOverlayType
Call
$this->context->getPropertyFromAspect('language', 'legacyOverlayType');

Returns one of

  • hideNonTranslated
  • 0 or
  • 1.

This property is kept for compatibility reasons. Do not use, if not really necessary, the option will be removed rather sooner than later.

Overlay types 

LanguageAspect::OVERLAYS_OFF
Just fetch records from the selected language as given by LanguageAspect->getContentId(). No overlay will happen, no fetching of the records from the default language. This boils down to "free mode" language handling. Records without a default language parent are included.
LanguageAspect::OVERLAYS_MIXED
Fetch records from the default language and overlay them with translations. If a record is not translated, the default language will be used.
LanguageAspect::OVERLAYS_ON
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be displayed.
LanguageAspect::OVERLAYS_ON_WITH_FLOATING
Fetch records from the default language and overlay them with translations. If a record is not translated, it will not be shown. Records without a default language parent are included.

Example 

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $fallbackChain = $this->context->getPropertyFromAspect(
            'language',
            'fallbackChain',
        );

        // ... do something with $fallbackChain
    }
}
Copied!

Preview aspect 

The preview aspect may be used to indicate that the frontend is in preview mode (for example, in case a workspace is previewed or hidden pages or records should be shown).

The preview aspect, \TYPO3\CMS\Frontend\Aspect\PreviewAspect , contains the following property:

isPreview

isPreview
Call
$this->context->getPropertyFromAspect('frontend.preview', 'isPreview');

Returns, whether the frontend is currently in preview mode.

User aspect 

Contains information about authenticated users in the current request. The aspect can be used for frontend and backend users.

The user aspect, \TYPO3\CMS\Core\Context\UserAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('frontend.user', 'id'); or $this->context->getPropertyFromAspect('backend.user', 'id');

Returns the uid of the currently logged in user, 0 if no user is logged in.

username

username
Call
$this->context->getPropertyFromAspect('frontend.user', 'username'); or $this->context->getPropertyFromAspect('backend.user', 'username');

Returns the username of the currently authenticated user. Empty string, if no user is logged in.

isLoggedIn

isLoggedIn
Call
$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn'); or $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn');

Returns, whether a user is logged in, as boolean.

isAdmin

isAdmin
Call
$this->context->getPropertyFromAspect('backend.user', 'isAdmin');

Returns, whether the user is an administrator, as boolean. It is only useful for backend users.

groupIds

groupIds
Call
$this->context->getPropertyFromAspect('frontend.user', 'groupIds'); or $this->context->getPropertyFromAspect('backend.user', 'groupIds');

Returns the groups the user is a member of, as array.

groupNames

groupNames
Call
$this->context->getPropertyFromAspect('frontend.user', 'groupNames'); or $this->context->getPropertyFromAspect('backend.user', 'groupNames');

Returns the names of all groups the user belongs to, as array.

Example 

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $userIsLoggedIn = $this->context->getPropertyFromAspect(
            'frontend.user',
            'isLoggedIn',
        );

        // ... do something with $userIsLoggedIn
    }
}
Copied!

Visibility aspect 

The aspect contains whether to show hidden pages, records (content) or even deleted records.

The visibility aspect, \TYPO3\CMS\Core\Context\VisibilityAspect , accepts the following properties:

includeHiddenPages

includeHiddenPages
Call
$this->context->getPropertyFromAspect('visibility', 'includeHiddenPages');

Returns, whether hidden pages should be displayed, as boolean.

includeHiddenContent

includeHiddenContent
Call
$this->context->getPropertyFromAspect('visibility', 'includeHiddenContent');

Returns, whether hidden content should be displayed, as boolean.

includeDeletedRecords

includeDeletedRecords
Call
$this->context->getPropertyFromAspect('visibility', 'includeDeletedRecords');

Returns, whether deleted records should be displayed, as boolean.

Example 

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'visibility',
            'includeHiddenPages',
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Workspace aspect 

The aspect contains information about the currently accessed workspace.

The workspace aspect, \TYPO3\CMS\Core\Context\WorkspaceAspect , accepts the following properties:

id

id
Call
$this->context->getPropertyFromAspect('workspace', 'id');

Returns the UID of the currently accessed workspace, as integer.

isLive

isLive
Call
$this->context->getPropertyFromAspect('workspace', 'isLive');

Returns whether the current workspace is live, or a custom offline workspace, as boolean.

isOffline

isOffline
Call
$this->context->getPropertyFromAspect('workspace', 'isOffline');

Returns, whether the current workspace is offline, as boolean.

Example 

EXT:my_extension/Classes/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use TYPO3\CMS\Core\Context\Context;

final class MyController
{
    public function __construct(
        private readonly Context $context,
    ) {}

    public function doSomething(): void
    {
        $showHiddenPages = $this->context->getPropertyFromAspect(
            'workspace',
            'id',
        );

        // ... do something with $showHiddenPages
    }
}
Copied!

Country API 

New in version 12.2

TYPO3 ships a list of countries of the world. The list is based on the ISO 3166-1 standard, with the alphanumeric short name ("FR" or "FRA" in its three-letter short name), the English name ("France"), the official name ("Republic of France"), also the numerical code, and the country's flag as emoji (UTF-8 representation).

Using the PHP API 

Dependency injection can be used to retrieve the \TYPO3\CMS\Core\Country\CountryProvider class:

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Country\CountryProvider;

final class MyClass
{
    public function __construct(
        private readonly CountryProvider $countryProvider,
    ) {}
}
Copied!

Get all countries 

To get all countries call the getAll() method:

EXT:my_extension/Classes/MyClass.php
$allCountries = $this->countryProvider->getAll();
Copied!

The method returns an array of \TYPO3\CMS\Core\Country\Country objects.

Get a country 

EXT:my_extension/Classes/MyClass.php
// Get the country by Alpha-2 code
$france = $this->countryProvider->getByIsoCode('FR');

// Get the country by name
$france = $this->countryProvider->getByEnglishName('France');

// Get the country by Alpha-3 code
$france = $this->countryProvider->getByAlpha3IsoCode('FRA');
Copied!

The methods return a \TYPO3\CMS\Core\Country\Country object.

Filter countries 

One can use filters to get the desired countries:

EXT:my_extension/Classes/MyClass.php
use TYPO3\CMS\Core\Country\CountryFilter;

$filter = new CountryFilter();

// Alpha-2 and Alpha-3 ISO codes can be used
$filter
    ->setOnlyCountries(['AT', 'DE', 'FR', 'DK'])
    ->setExcludeCountries(['AUT', 'DK']);

// Will be an array with "Germany" and "France"
$filteredCountries = $this->countryProvider->getFiltered($filter);
Copied!

The method getFiltered() return an array of \TYPO3\CMS\Core\Country\Country objects.

The Country object 

A country object can be used to fetch all information about it, also with translatable labels:

EXT:my_extension/Classes/MyClassWithTranslation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension;

use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locale;

final class MyClassWithTranslation
{
    public function __construct(
        private readonly CountryProvider $countryProvider,
        private readonly LanguageServiceFactory $languageServiceFactory,
    ) {}

    public function doSomething()
    {
        $languageService = $this->languageServiceFactory->create(new Locale('de'));
        $france = $this->countryProvider->getByIsoCode('FR');

        // "France"
        $france->getName();

        // "Frankreich"
        $languageService->sL($france->getLocalizedNameLabel());

        // "French Republic"
        echo $france->getOfficialName();

        // "Französische Republik"
        $languageService->sL($france->getLocalizedOfficialNameLabel());

        // 250
        $france->getNumericRepresentation();

        // "FR"
        $france->getAlpha2IsoCode();

        // "🇫🇷"
        $france->getFlag();
    }
}
Copied!

PHP API reference 

CountryProvider 

class CountryProvider
Fully qualified name
\TYPO3\CMS\Core\Country\CountryProvider

A class providing information about all countries.

Country data is generated from "Build/Scripts/updateIsoDatabase.php" (which in turn stems from https://github.com/sokil/php-isocodes-db-i18n)

getAll ( )
Returns
\Country[]
getByIsoCode ( string $isoCode)
param $isoCode

the isoCode

Returns
?\TYPO3\CMS\Core\Country\Country
getByAlpha2IsoCode ( string $isoCode)
param $isoCode

the isoCode

Returns
?\TYPO3\CMS\Core\Country\Country
getByAlpha3IsoCode ( string $isoCode)
param $isoCode

the isoCode

Returns
?\TYPO3\CMS\Core\Country\Country
getByEnglishName ( string $name)
param $name

the name

Returns
?\TYPO3\CMS\Core\Country\Country
getFiltered ( \TYPO3\CMS\Core\Country\CountryFilter $filter)
param $filter

the filter

Returns
array<string,\Country>

CountryFilter 

class CountryFilter
Fully qualified name
\TYPO3\CMS\Core\Country\CountryFilter

Filter object to limit countries to a subset of all countries.

getExcludeCountries ( )
returntype

array

setExcludeCountries ( array $excludeCountries)
param array $excludeCountries

the excludeCountries

returntype

TYPO3\CMS\Core\Country\CountryFilter

getOnlyCountries ( )
returntype

array

setOnlyCountries ( array $onlyCountries)
param array $onlyCountries

the onlyCountries

returntype

TYPO3\CMS\Core\Country\CountryFilter

Country 

class Country
Fully qualified name
\TYPO3\CMS\Core\Country\Country

DTO that keeps the information about a country. Never instantiate directly, use CountryProvider instead.

getName ( )
Returns
string
getLocalizedNameLabel ( )
Returns
string
getOfficialName ( )
Returns
?string
getLocalizedOfficialNameLabel ( )
Returns
string
getAlpha2IsoCode ( )
Returns
string
getAlpha3IsoCode ( )
Returns
string
getNumericRepresentation ( )
Returns
string
getFlag ( )
Returns
string

Form ViewHelper 

A Fluid ViewHelper is shipped with TYPO3 to render a dropdown for forms. See f:form.countrySelect for more information.

General Configuration 

The following examples are meant to add one single cropping configuration to sys_file_reference, which will then apply to every record referencing images.

In this example we configure two crop variants, one with the id "mobile", one with the id "desktop". The array key defines the crop variant id, which will be used when rendering an image with the image view helper.

For each crop variant there's at least one ratio configuration defined as allowedAspectRatios:

  • its key must not contain the dot character (.):

    • good examples: NaN, 4:3 or other-format
    • bad example: 1:1.441
  • its value is an array consisting of two keys:

    • title: should be a string (or even better: a LLL reference)
    • value: should be a float (not a string!)
'config' => [
     'type' => 'imageManipulation',
     'cropVariants' => [
         'mobile' => [
             'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
             'allowedAspectRatios' => [
                 '4:3' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                     'value' => 4 / 3
                 ],
                 'NaN' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                     'value' => 0.0
                 ],
             ],
         ],
         'desktop' => [
             'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.desktop',
             'allowedAspectRatios' => [
                 '4:3' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                     'value' => 4 / 3
                 ],
                 'NaN' => [
                     'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                     'value' => 0.0
                 ],
             ],
         ],
     ]
]
Copied!

Crop Area 

It is also possible to define an initial crop area. If no initial crop area is defined, the default selected crop area will cover the complete image. Crop areas are defined relatively with floating point numbers. The x and y coordinates and width and height must be specified for that. The below example has an initial crop area in the size the previous image cropper provided by default.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'cropArea' => [
                'x' => 0.1,
                'y' => 0.1,
                'width' => 0.8,
                'height' => 0.8,
            ],
        ],
    ],
]
Copied!

Focus Area 

Users can also select a focus area, when configured. The focus area is always inside the crop area and marks the area of the image which must be visible for the image to transport its meaning. The selected area is persisted to the database but will have no effect on image processing. The data points are however made available as data attribute when using the <f:image /> view helper and can be used by Javascript libraries.

The below example adds a focus area, which is initially one third of the size of the image and centered.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'focusArea' => [
                'x' => 1 / 3,
                'y' => 1 / 3,
                'width' => 1 / 3,
                'height' => 1 / 3,
            ],
        ],
    ],
]
Copied!

Cover Area 

Images are often used in a context where they are overlaid with other DOM elements like a headline. To give editors a hint which area of the image is affected, when selecting a crop area, it is possible to define multiple so-called cover areas. These areas are shown inside the crop area. The focus area cannot intersect with any of the cover areas.

'config' => [
    'type' => 'imageManipulation',
    'cropVariants' => [
        'mobile' => [
            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
            'coverAreas' => [
                [
                    'x' => 0.05,
                    'y' => 0.85,
                    'width' => 0.9,
                    'height' => 0.1,
                ]
            ],
        ],
    ],
]
Copied!

Rendering crop variants 

To render specific crop variants, the variant can be specified as argument of the image view helper:

<f:image image="{data.image}" cropVariant="mobile" width="800" />
Copied!

Crop variants configuration per content element 

It is possible to provide a configuration per content element. If you want a different cropping configuration for tt_content images, then you can add the following to your image field configuration of tt_content records:

'config' => [
    'overrideChildTca' => [
        'columns' => [
            'crop' => [
                'config' => [
                    'cropVariants' => [
                        'mobile' => [
                            'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
                            'cropArea' => [
                                'x' => 0.1,
                                'y' => 0.1,
                                'width' => 0.8,
                                'height' => 0.8,
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]
Copied!

Please note, you need to specify the target column name as array key. Most of the time this will be crop as this is the default field name for image manipulation in sys_file_reference

It is also possible to set the cropping configuration only for a specific tt_content element type by using the columnsOverrides feature:

$GLOBALS['TCA']['tt_content']['types']['textmedia']['columnsOverrides']['assets']['config']['overrideChildTca']['columns']['crop']['config'] = [
    'cropVariants' => [
       'default' => [
           'disabled' => true,
       ],
       'mobile' => [
           'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:imageManipulation.mobile',
           'cropArea' => [
               'x' => 0.1,
               'y' => 0.1,
               'width' => 0.8,
               'height' => 0.8,
           ],
           'allowedAspectRatios' => [
               '4:3' => [
                   'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
                   'value' => 4 / 3
               ],
               'NaN' => [
                   'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
                   'value' => 0.0
               ],
           ],
       ],
    ],
];
Copied!

Disable crop variants 

Please note, as the array for overrideChildTca is merged with the child TCA, so are the crop variants that are defined in the child TCA (most likely sys_file_reference). Because you cannot remove crop variants easily, it is possible to disable them for certain field types by setting the array key for a crop variant disabled to the value true as you can see in the example above for the default variant.

Database access and scheme in TYPO3 

Database structure and tables 

TYPO3 distinguishes between internal and managed tables.

Internal tables 

Used internally by TYPO3 and not accessible in the backend (e.g. tables such as be_sessions, sys_registry, cache tables). They are accessed via TYPO3 APIs such as the caching framework. These tables are not editable unless a specific backend module provides access.

Typical categories include:

  • Cache tables: Created automatically when using a database-based cache backend
  • Session tables: fe_sessions, be_sessions
  • System tables:

    • sys_registry: Global configuration
    • sys_log: Viewable via System > Log

Managed tables 

Defined in the TCA and, by default, editable in the Web > List module. TYPO3 derives database schemas from the TCA configuration. Required fields such as uid and pid are generated automatically.

Required fields:

  • uid: Primary key (auto-incremented)
  • pid: Page reference (from the pages table)

Typical fields:

  • title: Title displayed in backend lists
  • crdate: Creation timestamp
  • tstamp: Last modification timestamp
  • sorting: Manual sort order
  • deleted: Soft delete flag
  • hidden or disabled: Visibility control

These fields and their behavior are defined in the table properties (ctrl section of TCA).

When records are rendered in the backend using the FormEngine, entries with the soft delete flag set ( deleted) will not be shown.

When querying tables via TypoScript, visibility fields such as hidden, startdate, and enddate are respected.

If you use the DBAL query builder to access the database, the restriction builder automatically filters records based on visibility fields unless explicitly disabled.

When using an Extbase repository, the query settings also apply visibility constraints by default, but can be reconfigured to change this behavior.

The pages table 

Defines TYPO3's hierarchical page tree. All managed records reference a pages.uid via their pid.

  • The root page has pid = 0 and does not exist as a row in the table.
  • Only administrators can create records on the root level.
  • Tables must explicitly allow root-level records using -.

Many-to-many (MM) relations 

MM tables store relationships between records. Examples include:

  • sys_category_record_mm: Categories and categorized records
  • sys_file_reference: File usage in content and pages

These tables may appear in the backend if configured via inline records.

Database records 

In TYPO3, a record refers to an individual piece of content or data that is stored in the database. Each record is part of a table and represents a specific entity, such as a page, a content element, a backend user, or an extension configuration.

TYPO3 uses a modular structure where different types of data are managed as records, making it easy to organize and manipulate content.

Understanding records in TYPO3 is fundamental, as they are the building blocks for managing content and data within the system.

Common examples of records in TYPO3: 

Page records
These represent pages in the page tree, which structure the website. They are stored in table pages.
Content records
Every content record consists of sub entities like text, images, videos, and so on. Content records can be placed on a page. They are stored in table tt_content. TYPO3 has some pre configured content elements like for example Header Only, Regular Text Element, Text & Images, and Images Only.
Backend user records
The user records consist of information about the users who have access to the TYPO3 backend. They are stored in table be_users. Users are organized in user groups which are stored in table be_groups.
System records
System records control the configuration and management of the TYPO3 system. Examples include file references, file mounts, or categories. For example, you can create a category and assign it to some content records in order to indicate that they belong together.
Extension-specific records
Extensions often define custom records to store specific data, such as products for a shop system or events for a calendar.

Technical structure of a record: 

Each record is stored in a database table. Each row represents one record. Each column represents a field of the record or some kind of metadata.

A record typically includes a unique identifier in column uid, the id of the page record on which it is located in column pid, columns for various attributes (for example, title, content), and metadata like creation and modification timestamps, visibility, information on translation and workspace handling. A record can have relations to other records.

TCA (Table Configuration Array) 

TYPO3 uses the TCA to define how records of a specific table are structured, how they are displayed in the backend, and how they interact with other parts of the system. See the TCA Reference for details.

Types and subtypes in records 

In TYPO3, different types and subtypes of records are often stored in the same database table, even though not all types share the same columns. This approach allows for flexibility and efficiency in handling diverse content and data structures within a unified system.

TYPO3 uses a single-table inheritance strategy, where records of various types are distinguished by a specific field, often named type. For historical reasons the field is named CType for content elements and doktype for pages. The field that is used for the type is defined in TCA in ctrl > type. The types itself are stored in types.

This allows TYPO3 to store related records, such as different content types, in a shared table like tt_content while supporting custom fields for each record type.

For content elements in table tt_content there is a second level of subtypes in use where the field CType contains the value "list" and the field list-type contains the actual type. This second level of types exists for historic reasons. Read more about it in chapter Content Elements & Plugins.

Record objects 

New in version 13.2

Record objects have been introduced as an experimental feature.

Record objects are instances of \TYPO3\CMS\Core\Domain\Record and contain an object-oriented representation of a database record.

A record object can be used to output a database record in Fluid when no Extbase domain model is available.

Read more in chapter Record objects.

Extbase domain models 

TYPO3 extensions based on Extbase typically introduce a class inheriting from \TYPO3\CMS\Extbase\DomainObject\AbstractEntity to represent a record fully or partially within the domain of the extension. Multiple Extbase models can store their data in the same database table. Additionally, the same record can be represented in various ways by different Extbase models, depending on the specific requirements of each model.

See also chapter Extbase models.

Record objects 

New in version 13.2

Record objects have been introduced as an experimental feature.

Record objects are instances of \TYPO3\CMS\Core\Domain\Record .

They are an advanced data object holding the data of a database row, taking the TCA definition and possible relations of that database row into account.

Provide Records in TypoScript 

In TypoScript you can use the RecordTransformationProcessor, usually in combination with the DatabaseQueryProcessor to pass record objects to the Fluid templating engine.

Provide records in PHP 

In PHP a record object can be created by the \TYPO3\CMS\Core\Domain\RecordFactory .

The event RecordCreationEvent can be used to influence or replace the Record object and its properties during creation.

Use records in Fluid 

In frontend templates the record object is provided by TypoScript or passed to Fluid by a PHP class.

Content element preview templates automatically receive a record object representing the record of the content element that should currently be displayed.

The Debug ViewHelper <f:debug> output of the Record object is misleading for integrators, as most properties are accessed differently as one would assume.

We are dealing with an object here. You however can access your record properties as you are used to with {record.title} or {record.uid}. In addition, you gain special, context-aware properties like the language {record.languageId} or workspace {data.versionInfo.workspaceId}.

Overview of all possibilities:

Demonstration of available variables in Fluid
<!-- Any property, which is available in the Record (like normal) -->
{record.title}
{record.uid}
{record.pid}

<!-- Language related properties -->
{record.languageId}
{record.languageInfo.translationParent}
{record.languageInfo.translationSource}

<!-- The overlaid uid -->
{record.overlaidUid}

<!-- Types are a combination of the table name and the Content Type name. -->
<!-- Example for table "tt_content" and CType "textpic": -->

<!-- "tt_content" (this is basically the table name) -->
{record.mainType}

<!-- "textpic" (this is the CType) -->
{record.recordType}

<!-- "tt_content.textpic" (Combination of mainType and record type, separated by a dot) -->
{record.fullType}

<!-- System related properties -->
{record.systemProperties.isDeleted}
{record.systemProperties.isDisabled}
{record.systemProperties.isLockedForEditing}
{record.systemProperties.createdAt}
{record.systemProperties.lastUpdatedAt}
{record.systemProperties.publishAt}
{record.systemProperties.publishUntil}
{record.systemProperties.userGroupRestriction}
{record.systemProperties.sorting}
{record.systemProperties.description}

<!-- Computed properties depending on the request context -->
{record.computedProperties.versionedUid}
{record.computedProperties.localizedUid}
{record.computedProperties.requestedOverlayLanguageId}
{record.computedProperties.translationSource} <!-- Only for pages, contains the Page model -->

<!-- Workspace related properties -->
{record.versionInfo.workspaceId}
{record.versionInfo.liveId}
{record.versionInfo.state.name}
{record.versionInfo.state.value}
{record.versionInfo.stageId}
Copied!

Using the raw record 

The RecordFactory object contains only the properties, relevant for the current record type, for example CType. In case you need to access properties, which are not defined for the record type, the "raw" record can be used by accessing it via {record.rawRecord}. Those properties are not transformed.

Database (Doctrine DBAL) 

This chapter describes accessing the database on the level of the Doctrine Database Abstraction Layer (DBAL).

The Doctrine Database Abstraction Layer (DBAL) in TYPO3 provides developers with a powerful and flexible way to interact a database, allowing them to perform database operations through an object-oriented API while ensuring compatibility across different database systems.

In the TYPO3 backend rows of database tables are usually represented as Database records and configured in TCA (Table Configuration Array).

In Extbase extensions tables are abstracted as Extbase models. Operations such as creating, updating and deleting database records are usually performed from within a Extbase repository with methods provided by Extbase classes. However, Doctrine DBAL can also be used by extensions that use, for example, an Extbase controller.

Introduction 

TYPO3 relies on storing its data in a relational database management system (RDBMS). The Doctrine DBAL component is used to enable connecting to different database management systems. Most used is still MySQL / MariaDB, but thanks to Doctrine others like PostgreSQL and SQLite are also an option.

The corresponding DBMS can be selected during installation.

This chapter gives an overview of the basic TYPO3 database table structure, followed by some information on upgrading and maintaining table and field consistency, and then deep dives into the programming API.

Doctrine DBAL 

Database queries in TYPO3 are done with an API based on Doctrine DBAL. The API is provided by the system extension core, which is always loaded and thus always available.

Extension authors can use this low-level API to manage query operations directly on the configured DBMS.

Doctrine DBAL is rich in features. Drivers for various target systems enable TYPO3 to run on a long list of ANSI SQL-compatible DBMSes. If used properly, queries created with this API are translated to the specific database engine by Doctrine without an extension developer taking care of that specifically.

The API provided by the Core is basically a pretty small and lightweight facade in front of Doctrine DBAL that adds some convenient methods as well as some TYPO3-specific sugar. The facade additionally provides methods to retrieve specific connection objects per configured database connection based on the table that is queried. This enables instance administrators to configure different database engines for different tables, while being transparent to extension developers.

This document does not outline every single method that the API provides. It sticks to those that are commonly used in extensions, and some parts like the rewritten schema migrator are omitted as they are usually of little to no interest to extensions.

Understanding Doctrine DBAL and Doctrine ORM 

Doctrine is a two-part project, with Doctrine DBAL being the low-level database abstraction and the interface for building queries to specific database engines, while Doctrine ORM is a high-level object relational mapping on top of Doctrine DBAL.

The TYPO3 Core implements - only - the DBAL part. Doctrine ORM is neither required nor implemented nor used.

Low-level and high-level database calls 

This documentation focuses on low-level database calls. In many cases, it is better to use higher level APIs such as the DataHandler or Extbase repositories and to let the framework handle persistence details internally.

Configuration 

The configuration of Doctrine DBAL for TYPO3 is about specifying the single database endpoints and passing the connection credentials. The framework supports the parallel usage of multiple database connections, a specific connection is mapped depending on its table name. The table space can be seen as a transparent layer that determines which specific connection is chosen for a query to a single or a group of tables: It allows "swapping out" single tables from the Default connection to point them to a different database endpoint.

As with other central configuration options, the database endpoint and mapping configuration is done in config/system/settings.php and ends up in $GLOBALS['TYPO3_CONF_VARS'] after the Core bootstrap. The specific sub-array is $GLOBALS['TYPO3_CONF_VARS']['DB'] .

Example: one connection 

A typical basic example using only the Default connection with a single database endpoint:

config/system/settings.php
// [...]
'DB' => [
    'Connections' => [
        'Default' => [
            'charset' => 'utf8',
            'dbname' => 'theDatabaseName',
            'driver' => 'mysqli',
            'host' => 'theHost',
            'password' => 'theConnectionPassword',
            'port' => 3306,
            'user' => 'theUser',
        ],
    ],
],
// [...]
Copied!

Remarks:

  • The Default connection must be configured, this can not be left out or renamed.
  • For the mysqli driver: If the host is set to localhost and if the default PHP options in this area are not changed, the connection will be socket-based. This saves a little overhead. To force a TCP/IP-based connection even for localhost, the IPv4 address 127.0.0.1 or IPv6 address ::1/128 respectively must be used as host value.
  • The connection options are passed to Doctrine DBAL without much manipulation from TYPO3 side. Please refer to the doctrine connection docs for a full overview of the settings.
  • If the charset option is not specified, it defaults to utf8.
  • The option wrapperClass is used by TYPO3 to insert the extended Connection class \TYPO3\CMS\Core\Database\Connection as main facade around Doctrine DBAL.

Example: two connections 

Another example with two connections, where the be_sessions table is mapped to a different endpoint:

config/system/settings.php
// [...]
'DB' => [
    'Connections' => [
        'Default' => [
            'charset' => 'utf8',
            'dbname' => 'default_dbname',
            'driver' => 'mysqli',
            'host' => 'default_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'default_user',
        ],
        'Sessions' => [
            'charset' => 'utf8mb4',
            'driver' => 'mysqli',
            'dbname' => 'sessions_dbname',
            'host' => 'sessions_host',
            'password' => '***',
            'port' => 3306,
            'user' => 'some_user',
        ],
    ],
    'TableMapping' => [
        'be_sessions' => 'Sessions',
    ]
],
// [...]
Copied!

Remarks:

  • The array key Sessions is just a name. It can be different, but it is good practice to give it a useful, descriptive name.
  • It is possible to map multiple tables to a different endpoint by adding further table name / connection name pairs to TableMapping.

Basic create, read, update, and delete operations (CRUD) 

This section provides a list of basic usage examples of the query API. This is just a starting point. Details about the single methods can be found in the following chapters, especially QueryBuilder and Connection.

All examples use dependency injection to provide the ConnectionPool in the classes.

Insert a row 

A direct insert into a table:

EXT:my_extension/Classes/Domain/Repository/MyInsertRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyInsertRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomeData(): void
    {
        $this->connectionPool
            ->getConnectionForTable('tt_content')
            ->insert(
                'tt_content',
                [
                    'pid' => 42,
                    'bodytext' => 'ipsum',
                ],
            );
    }
}
Copied!

This results in the following SQL statement:

INSERT INTO `tt_content` (`pid`, `bodytext`)
    VALUES ('42', 'ipsum')
Copied!

Select a single row 

Fetching a single row directly from the tt_content table:

EXT:my_extension/Classes/Domain/Repository/MySelectRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MySelectRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    /**
     * @return array|false
     */
    public function selectSomeData()
    {
        $uid = 4;

        return $this->connectionPool
            ->getConnectionForTable('tt_content')
            ->select(
                ['uid', 'pid', 'bodytext'], // fields to select
                'tt_content',               // from
                ['uid' => $uid],            // where
            )
            ->fetchAssociative();
    }
}
Copied!

Result in $row:

array(3 items)
   uid => 4 (integer)
   pid => 35 (integer)
   bodytext => 'some content' (12 chars)
Copied!

The engine encloses field names in quotes, adds default TCA restrictions such as deleted=0, and prepares a query to be executed with this final statement:

SELECT `uid`, `pid`, `bodytext`
    FROM `tt_content`
    WHERE (`uid` = '4')
        AND ((`tt_content`.`deleted` = 0)
        AND (`tt_content`.`hidden` = 0)
        AND (`tt_content`.`starttime` <= 1669838885)
        AND ((`tt_content`.`endtime` = 0) OR (`tt_content`.`endtime` > 1669838885)))
Copied!

Select multiple rows with some "where" magic 

Advanced query using the QueryBuilder and manipulating the default restrictions:

EXT:my_extension/Classes/Domain/Repository/MyQueryBuilderSelectRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyQueryBuilderSelectRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function selectSomeData(): array
    {
        $uid = 4;

        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable('tt_content');

        // Remove all default restrictions (delete, hidden, starttime, stoptime),
        // but add DeletedRestriction again
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));

        // Execute a query with "bodytext=lorem OR uid=4" and proper quoting
        return $queryBuilder
            ->select('uid', 'pid', 'bodytext')
            ->from('tt_content')
            ->where(
                $queryBuilder->expr()->or(
                    $queryBuilder->expr()->eq(
                        'bodytext',
                        $queryBuilder->createNamedParameter('lorem'),
                    ),
                    $queryBuilder->expr()->eq(
                        'uid',
                        $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT),
                    ),
                ),
            )
            ->executeQuery()
            ->fetchAllAssociative();
    }
}
Copied!

Result in $rows:

array(2 items)
    0 => array(3 items)
        uid => 4 (integer)
        pid => 35 (integer)
        bodytext => 'ipsum' (5 chars)
    1 => array(3 items)
        uid => 366 (integer)
        pid => 13 (integer)
        bodytext => 'lorem' (5 chars)
Copied!

The executed query looks like this:

SELECT `uid`, `pid`, `bodytext`
    FROM `tt_content`
    WHERE ((`bodytext` = 'lorem') OR (`uid` = 4))
        AND (`tt_content`.`deleted` = 0)
Copied!

Update multiple rows 

EXT:my_extension/Classes/Domain/Repository/MyUpdateRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyUpdateRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function updateSomeData()
    {
        $this->connectionPool->getConnectionForTable('tt_content')
            ->update(
                'tt_content',
                [ 'bodytext' => 'ipsum' ], // set
                [ 'bodytext' => 'lorem' ], // where
            );
    }
}
Copied!

The executed query looks like this:

UPDATE `tt_content` SET `bodytext` = 'ipsum'
    WHERE `bodytext` = 'lorem'
Copied!

Delete a row 

EXT:my_extension/Classes/Domain/Repository/MyDeleteRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyDeleteRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function deleteSomeData()
    {
        $this->connectionPool->getConnectionForTable('tt_content')
            ->delete(
                'tt_content', // from
                ['uid' => 4711],  // where
            );
    }
}
Copied!

The executed query looks like this:

DELETE FROM `tt_content`
    WHERE `uid` = '4711'
Copied!

Class overview 

Doctrine DBAL provides a set of PHP objects to represent, create and handle SQL queries and their results. The basic class structure has been slightly enriched by TYPO3 to add CMS-specific features. Extension authors will usually interact with these classes and objects:

\TYPO3\CMS\Core\Database\Connection
This object represents a specific connection to one connected database. It provides "shortcut" methods for simple standard queries like SELECT or UPDATE. To create more complex queries, an instance of the QueryBuilder can be retrieved.
\TYPO3\CMS\Core\Database\ConnectionPool
The ConnectionPool is the main entry point for extensions to retrieve a specific connection over which to execute a query. Usually it is used to return a Connection or a QueryBuilder object.
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
The ExpressionBuilder object is used to model complex expressions. It is mainly used for WHERE and JOIN conditions.
\TYPO3\CMS\Core\Database\Query\QueryBuilder
With the help of the QueryBuilder one can create all sort of complex queries executed on a specific connection. It provides the main CRUD methods for select(), delete() and friends.
TYPO3\CMS\Core\Database\Query\Restriction\...
Restrictions are a set of classes that add expressions like deleted=0 to a query, based on the TCA settings of a table. They automatically adds TYPO3-specific restrictions like start time and end time, as well as deleted and hidden flags. Further restrictions for language overlays and workspaces are available. In this documentation, these classes are referred as RestrictionBuilder.
\Doctrine\DBAL\Driver\Statement
This result object is returned when a SELECT or COUNT query was executed. Single rows are returned as an array by calling ->fetchAssociative() until the method returns false.

ConnectionPool 

TYPO3's interface for executing queries via Doctrine DBAL starts with a request to the ConnectionPool for a QueryBuilder or a Connection object and passing the table name to be queried:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_domain_model_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        // Get a query builder for a table
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        // Or get a connection for a table
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
    }
}
Copied!

The QueryBuilder is the default object used by extension authors to express complex queries, while a Connection instance can be used as a shortcut to handle some simple query cases.

Pooling: multiple connections to different database endpoints 

TYPO3 can handle multiple connections to different database endpoints at the same time. This can be configured for each individual table in $GLOBALS['TYPO3_CONF_VARS'] (see database configuration for details). This makes it possible to run tables on different databases without an extension developer having to worry about it.

The ConnectionPool implements this feature: It looks for configured table-to-database mapping and can return a Connection or a QueryBuilder instance for that specific connection. These objects know internally which target connection they are dealing with and will quote field names accordingly, for instance.

Beware 

However, the transparency of tables for different database endpoints is limited.

Executing a table JOIN between two tables that reference different connections will result in an exception. This restriction may in practice lead to implicit "groups" of tables that must to point to a single connection when an extension or the TYPO3 Core joins these tables.

This can be problematic when several different extensions use, for instance, the Core category or collection API with their mm table joins between Core internal tables and their extension counterparts.

That situation is not easy to deal with. At the time of writing the Core development will implement eventually some non-join fallbacks for typical cases that would be good to decouple, though.

Query builder 

The query builder provides a set of methods to create queries programmatically.

This chapter provides examples of the most common queries.

The query builder comes with a happy little list of small methods:

  • Set type of query: ->select(), ->count(), ->update(), ->insert() and ->delete()
  • Prepare WHERE conditions
  • Manipulate default WHERE restrictions added by TYPO3 for ->select()
  • Add LIMIT, GROUP BY and other SQL functions
  • executeQuery() executes a SELECT query and returns a result, a \Doctrine\DBAL\Result object
  • executeStatement() executes an INSERT, UPDATE or DELETE statement and returns the number of affected rows.

Most of the query builder methods provide a fluent interface, return an instance of the current query builder itself, and can be chained:

$queryBuilder
    ->select('uid')
    ->from('pages');
Copied!

Instantiation 

To create an instance of the query builder, call ConnectionPool::getQueryBuilderForTable() and pass the table as an argument. The ConnectionPool object can be injected via dependency injection.

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyRepository
{
    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable('aTable');
    }
}
Copied!

select() and addSelect() 

Create a SELECT query.

Select all fields:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT *
$queryBuilder->select('*')
Copied!

->select() and a number of other methods of the query builder are variadic and can handle any number of arguments. In ->select() each argument is interpreted as a single field name to be selected:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid`, `pid`, `aField`
$queryBuilder->select('uid', 'pid', 'aField');
Copied!

Argument unpacking can be used if the list of fields already is available as array:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid`, `pid`, `aField`, `anotherField`
$fields = ['uid', 'pid', 'aField', 'anotherField'];
$queryBuilder->select(...$fields);
Copied!

->select() automatically supports AS and quotes identifiers. This can be especially useful for join() operations:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `tt_content`.`bodytext` AS `t1`.`text`
$queryBuilder->select('tt_content.bodytext AS t1.text')
Copied!

With ->select() the list of fields to be selected is specified, and with ->addSelect() further elements can be added to an existing list.

Mind that ->select() replaces any formerly registered list instead of appending it. Thus, it is not very useful to call select() twice in a code flow or after an ->addSelect(). The methods ->where() and ->andWhere() share the same behavior: ->where() replaces all formerly registered constraints, ->andWhere() appends additional constraints.

A useful combination of ->select() and ->addSelect() can be:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder->select(...$defaultList);
if ($needAdditionalFields) {
    $queryBuilder->addSelect(...$additionalFields);
}
Copied!

Calling the executeQuery() function on a ->select() query returns a result object of type \Doctrine\DBAL\Result. To receive single rows, a ->fetchAssociative() loop is used on that object, or ->fetchAllAssociative() to return a single array with all rows. A typical code flow of a SELECT query looks like this:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'header', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // Do something with that single row
    debug($row);
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Default Restrictions 

count() 

Create a COUNT query, a typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT COUNT(`uid`) FROM `tt_content` WHERE (`bodytext` = 'lorem')
//     AND ((`tt_content`.`deleted` = 0) AND (`tt_content`.`hidden` = 0)
//     AND (`tt_content`.`starttime` <= 1669885410)
//     AND ((`tt_content`.`endtime` = 0) OR (`tt_content`.`endtime` > 1669885410)))
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$count = $queryBuilder
    ->count('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeQuery()
    ->fetchOne();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • Similar to the ->select() query type, ->count() automatically triggers the magic of the RestrictionBuilder that adds default restrictions such as deleted, hidden, starttime and endtime when defined in TCA.
  • Similar to ->select() query types, ->executeQuery() with ->count() returns a result object of type \Doctrine\DBAL\Result. To fetch the number of rows directly, use ->fetchOne().
  • The first argument to ->count() is required, typically ->count(*) or ->count('uid') is used, the field name is automatically quoted.
  • There is no support for DISTINCT, instead a ->groupBy() has to be used, for example:

    // Equivalent to:
    // SELECT DISTINCT some_field, another_field FROM my_table
    
    $queryBuilder
        ->select('some_field', 'another_field')
        ->from('my_table')
        ->groupBy('some_field')
        ->addGroupBy('another_field');
    Copied!
  • If ->count() is combined with ->groupBy(), the result may return multiple rows. The order of those rows depends on the used DBMS. Therefore, to ensure the same order of result rows on multiple different databases, a ->groupBy() should always be combined with an ->orderBy().

delete() 

Create a DELETE FROM query. The method requires the table name from which data is to be deleted. Classic usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// DELETE FROM `tt_content` WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$affectedRows = $queryBuilder
    ->delete('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • For simple cases it is often easier to write and read using the ->delete() method of the Connection object.
  • In contrast to ->select(), ->delete() does not automatically add WHERE restrictions like AND `deleted` = 0.
  • ->delete() does not magically transform a DELETE FROM `tt_content` WHERE `uid` = 4711 into something like UPDATE `tt_content` SET `deleted` = 1 WHERE `uid` = 4711 internally. A soft-delete must be handled at application level with a dedicated lookup in $GLOBALS['TCA']['theTable']['ctrl']['delete'] to check if a specific table can handle the soft-delete, together with an ->update() instead.
  • Deleting from multiple tables at once is not supported: DELETE FROM `table1`, `table2` can not be created.
  • ->delete() ignores ->join()
  • ->delete() ignores setMaxResults(): DELETE with LIMIT does not work.

update() and set() 

Create an UPDATE query. Typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` SET `bodytext` = 'dolor' WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('bodytext', 'dolor')
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

->update() requires the table to update as the first argument and a table alias (for example, t) as optional second argument. The table alias can then be used in ->set() and ->where() expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` `t` SET `t`.`bodytext` = 'dolor' WHERE `t`.`bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content', 't')
    ->where(
        $queryBuilder->expr()->eq('t.bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('t.bodytext', 'dolor')
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

->set() requires a field name as the first argument and automatically quotes it internally. The second mandatory argument is the value to set a field to. The value is automatically transformed to a named parameter of a prepared statement. This way, ->set() key/value pairs are automatically SQL protected from injection by default.

If a field should be set to the value of another field from the row, quoting must be turned off and ->quoteIdentifier() and false have to be used:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// UPDATE `tt_content` SET `bodytext` = `header` WHERE `bodytext` = 'lorem'
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->update('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR))
    )
    ->set('bodytext', $queryBuilder->quoteIdentifier('header'), false)
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • For simple cases it is often easier to use the ->update() method of the Connection object.
  • ->set() can be called multiple times if multiple fields should be updated.
  • ->set() requires a field name as the first argument and automatically quotes it internally.
  • ->set() requires the value to which a field is to be set as the second parameter.
  • ->update() ignores ->join() and ->setMaxResults().
  • The API does not magically add deleted = 0 or other restrictions, as is currently the case with select, for example. (See also RestrictionBuilder).

insert() and values() 

Create an INSERT query. Typical usage:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// INSERT INTO `tt_content` (`bodytext`, `header`) VALUES ('lorem', 'dolor')
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$affectedRows = $queryBuilder
    ->insert('tt_content')
    ->values([
        'bodytext' => 'lorem',
        'header' => 'dolor',
    ])
    ->executeStatement();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • The uid of the created database row can be fetched from the connection by using $queryBuilder->getConnection()->lastInsertId().
  • ->values() expects an array of key/value pairs. Both keys (field names / identifiers) and values are automatically quoted. In rare cases, quoting of values can be turned off by setting the second argument to false. Then quoting must be done manually, typically by using ->createNamedParameter() on the values.
  • ->executeStatement() after ->insert() returns the number of inserted rows, which is typically 1.
  • An alternative to using the query builder for inserting data is using the Connection object with its ->insert() method.
  • The query builder does not provide a method for inserting multiple rows at once, use ->bulkInsert() of the Connection object instead to achieve that.

from() 

->from() is essential for ->select() and ->count() query types. ->from() requires a table name and an optional alias name. The method is usually called once per query creation and the table name is usually the same as the one passed to ->getQueryBuilderForTable(). If the query joins multiple tables, the argument should be the name of the first table within the ->join() chain:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// FROM `myTable`
$queryBuilder->from('myTable');

// FROM `myTable` AS `anAlias`
$queryBuilder->from('myTable', 'anAlias');
Copied!

->from() can be called multiple times and will create the Cartesian product of tables if not constrained by a respective ->where() or ->andWhere() expression. In general, it is a good idea to use ->from() only once per query and instead model the selection of multiple tables with an explicit ->join().

where(), andWhere() and orWhere() 

The three methods are used to create WHERE restrictions for SELECT, COUNT, UPDATE and DELETE query types. Each argument is usually an ExpressionBuilder object that is converted to a string on ->executeQuery() or ->executeStatement():

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT `uid`, `header`, `bodytext`
// FROM `tt_content`
// WHERE
//    (
//       ((`bodytext` = 'lorem') AND (`header` = 'a name'))
//       OR (`bodytext` = 'dolor') OR (`bodytext` = 'hans')
//    )
//    AND (`pid` = 42)
//    AND ... RestrictionBuilder TCA restrictions ...
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'header', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR)),
        $queryBuilder->expr()->eq('header', $queryBuilder->createNamedParameter('a name', Connection::PARAM_STR))
    )
    ->orWhere(
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('dolor')),
        $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('hans'))
    )
    ->andWhere(
        $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Note the parenthesis of the above example: ->andWhere() encapsulates both ->where() and ->orWhere() with an additional restriction.

Argument unpacking can become handy with these methods:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$whereExpressions = [
    $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR)),
    $queryBuilder->expr()->eq('header', $queryBuilder->createNamedParameter('a name', Connection::PARAM_STR))
];
if ($needsAdditionalExpression) {
    $whereExpressions[] = $someAdditionalExpression;
}
$queryBuilder->where(...$whereExpressions);
Copied!

See available parameter types.

Remarks:

  • The three methods are variadic. They can handle any number of arguments. For instance, if ->where() receives four arguments, they are handled as single expressions, all combined with AND.
  • createNamedParameter is used to create a placeholder for a field value of a prepared statement. Always use this when dealing with user input in expressions to protect the statement from SQL injections.
  • ->where() should be called only once per query and resets all previously set ->where(), ->andWhere() and ->orWhere() expressions. A ->where() call after a previous ->where(), ->andWhere() or ->orWhere() usually indicates a bug or a rather weird code flow. Doing so is discouraged.
  • When creating complex WHERE restrictions, ->getSQL() and ->getParameters() are helpful debugging tools to verify parenthesis and single query parts.
  • If only ->eq() expressions are used, it is often easier to switch to the according method of the Connection object to simplify quoting and improve readability.
  • It is possible to feed the methods directly with strings, but this is discouraged and usually used only in rare cases where expression strings are created in a different place that can not be easily resolved.

join(), innerJoin(), rightJoin() and leftJoin() 

Joining multiple tables in a ->select() or ->count() query is done with one of these methods. Multiple joins are supported by calling the methods more than once. All methods require four arguments: The name of the table on the left (or its alias), the name of the table on the right, an alias for the name of the table on the right, and the join restriction as fourth argument:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `sys_language`.`uid`, `sys_language`.`title`
// FROM `sys_language`
// INNER JOIN `pages` `p`
//     ON `p`.`sys_language_uid` = `sys_language`.`uid`
// WHERE
//     (`p`.`uid` = 42)
//     AND (
//          (`p`.`deleted` = 0)
//          AND (
//              (`sys_language`.`hidden` = 0) AND (`overlay`.`hidden` = 0)
//          )
//          AND (`p`.`starttime` <= 1475591280)
//          AND ((`p`.`endtime` = 0) OR (`overlay`.`endtime` > 1475591280))
//     )
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language')
$result = $queryBuilder
   ->select('sys_language.uid', 'sys_language.title')
   ->from('sys_language')
   ->join(
       'sys_language',
       'pages',
       'p',
       $queryBuilder->expr()->eq('p.sys_language_uid', $queryBuilder->quoteIdentifier('sys_language.uid'))
   )
   ->where(
       $queryBuilder->expr()->eq('p.uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))
   )
   ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Notes to the example above:

  • The query operates with the sys_language table as the main table, this table name is given to getQueryBuilderForTable().
  • The query joins the pages table as INNER JOIN and gives it the alias p.
  • The join condition is `p`.`sys_language_uid` = `sys_language`.`uid`. It would have been identical to swap the expression arguments of the fourth ->join() argument ->eq('sys_language.uid', $queryBuilder->quoteIdentifier('p.sys_language_uid')).
  • The second argument of the join expression instructs the ExpressionBuilder to quote the value as a field identifier (a field name, here a combination of table and field name). Using createNamedParameter would lead in quoting as value (' instead of ` in MySQL) and the query would fail.
  • The alias p - the third argument of the ->join() call - does not necessarily have to be set to a different name than the table name itself here. It is sufficient to use pages as third argument and not to specify any other name. Aliases are mostly useful when a join to the same table is needed: SELECT `something` FROM `tt_content` JOIN `tt_content` `content2` ON .... Aliases are also useful to increase the readability of ->where() expressions.
  • The RestrictionBuilder has added additional WHERE conditions for both tables involved! The sys_language table obviously only specifies a 'disabled' => 'hidden' as enableColumns in its TCA ctrl section, while the pages table specifies the fields deleted, hidden, starttime and stoptime.

A more complex example with two joins. The first join points to the first table, again using an alias to resolve a language overlay scenario. The second join uses the alias of the first join target as left side:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `tt_content_orig`.`sys_language_uid`
// FROM `tt_content`
// INNER JOIN `tt_content` `tt_content_orig` ON `tt_content`.`t3_origuid` = `tt_content_orig`.`uid`
// INNER JOIN `sys_language` `sys_language` ON `tt_content_orig`.`sys_language_uid` = `sys_language`.`uid`
// WHERE
//     (`tt_content`.`colPos` = 1)
//     AND (`tt_content`.`pid` = 42)
//     AND (`tt_content`.`sys_language_uid` = 2)
//     AND ... RestrictionBuilder TCA restrictions for tables tt_content and sys_language ...
// GROUP BY `tt_content_orig`.`sys_language_uid`
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language')
$constraints = [
    $queryBuilder->expr()->eq('tt_content.colPos', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
    $queryBuilder->expr()->eq('tt_content.pid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)),
    $queryBuilder->expr()->eq('tt_content.sys_language_uid', $queryBuilder->createNamedParameter(2, Connection::PARAM_INT)),
];
$queryBuilder
    ->select('tt_content_orig.sys_language_uid')
    ->from('tt_content')
    ->join(
        'tt_content',
        'tt_content',
        'tt_content_orig',
        $queryBuilder->expr()->eq(
            'tt_content.t3_origuid',
            $queryBuilder->quoteIdentifier('tt_content_orig.uid')
        )
    )
    ->join(
        'tt_content_orig',
        'sys_language',
        'sys_language',
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        )
    )
    ->where(...$constraints)
    ->groupBy('tt_content_orig.sys_language_uid')
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Further remarks:

  • ->join() and innerJoin are identical. They create an INNER JOIN query, this is identical to a JOIN query.
  • ->leftJoin() creates a LEFT JOIN query, this is identical to a LEFT OUTER JOIN query.
  • ->rightJoin() creates a RIGHT JOIN query, this is identical to a RIGHT OUTER JOIN query.
  • Calls to join() methods are only considered for ->select() and ->count() type queries. ->delete(), ->insert() and update() do not support joins, these query parts are ignored and do not end up in the final statement.
  • The argument of ->getQueryBuilderForTable() should be the leftmost main table.
  • Joining two tables that are configured to different connections will throw an exception. This restricts the tables that can be configured for different database endpoints. It is possible to test the connection objects of the involved tables for equality and implement a fallback logic in PHP if they are different.
  • Doctrine DBAL does not support the use of join methods in combination with ->update(), ->insert() and ->delete() methods, because such a statement is not cross-platform compatible.
  • Multiple join condition expressions can be resolved as strings like:

    $joinConditionExpression = $queryBuilder->expr()->and(
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        ),
        $queryBuilder->expr()->eq(
            'tt_content_orig.sys_language_uid',
            $queryBuilder->quoteIdentifier('sys_language.uid')
        ),
    );
    $queryBuilder->leftJoin(
        'tt_content_orig',
        'sys_language',
        'sys_language',
        (string)$joinConditionExpression
    );
    Copied!

orderBy() and addOrderBy() 

Add ORDER BY to a ->select() statement. Both ->orderBy() and ->addOrderBy() require a field name as first argument:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` ORDER BY `sorting` ASC
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder->getRestrictions()->removeAll();
$languageRecords = $queryBuilder
    ->select('*')
    ->from('sys_language')
    ->orderBy('sorting')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • ->orderBy() resets all previously specified orders. It makes no sense to call this function again after a previous ->orderBy() or ->addOrderBy().
  • Both methods need a field name or a table.fieldName or a tableAlias.fieldName as first argument. In the example above the call to ->orderBy('sys_language.sorting') would have been identical. All identifiers are quoted automatically.
  • The second, optional argument of both methods specifies the sort order. The two allowed values are 'ASC' and 'DESC', where 'ASC' is default and can be omitted.
  • To create a chain of orders, use ->orderBy() and then multiple ->addOrderBy() calls. The call to ->orderBy('header')->addOrderBy('bodytext')->addOrderBy('uid', 'DESC') creates ORDER BY `header` ASC, `bodytext` ASC, `uid` DESC
  • To achieve more complex sortings, which can't be created with QueryBuilder, you can fall back on the underlying raw Doctrine QueryBuilder. This is accessible with ->getConcreteQueryBuilder(). It doesn't do any quoting, so you can do something like $concreteQueryBuilder->orderBy('FIELD(eventtype, 0, 4, 1, 2, 3)');. Make sure to quote properly as this is entirely your responsibility with the Doctrine QueryBuilder!

groupBy() and addGroupBy() 

Add GROUP BY to a ->select() statement. Each argument of the methods is a single identifier:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// GROUP BY `pages`.`sys_language_uid`, `sys_language`.`uid`
->groupBy('pages.sys_language_uid', 'sys_language.uid');
Copied!

Remarks:

  • Similar to ->select() and ->where(), both methods are variadic and take any number of arguments, argument unpacking is supported: ->groupBy(...$myGroupArray)
  • Each argument is either a direct field name GROUP BY `bodytext`, a table.fieldName or a tableAlias.fieldName and is properly quoted.
  • ->groupBy() resets all previously defined group specification and should only be called once per statement.
  • For more complex statements you can use the raw Doctrine QueryBuilder. See remarks for orderBy()

union() and addUnion() 

Method union() provides a streamlined way to combine result sets from multiple queries.

union(string|QueryBuilder $part)
Creates the initial UNION query part by accepting either a raw SQL string or a QueryBuilder instance. Calling union() resets all previous union definitions, it should therefore only be called once, using addUnion() to add subsequent union parts.
addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)

Adds additional UNION parts to the query. The $type parameter accepts:

UnionType::DISTINCT
Combines results while eliminating duplicates.
UnionType::ALL
Combines results and retains all duplicates. Not removing duplicates can be a performance improvement.

Named placeholders, such as created by QueryBuilder::createNamedParameter() must be created on the outer most QueryBuilder See the example below.

Database provider support of union() and addUnion() 

QueryBuilder can be used create UNION clause queries not compatible with all database providers, for example using LIMIT/OFFSET in each part query or other stuff.

When building functional tests, run them on all database types that should be supported.

Example using union() on two QueryBuilders 

packages/my_extension/classes/Service/MyService.php
<?php

namespace MyExtension\MyVendor\Service;

use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final readonly class MyService
{
    public function __construct(
        private ConnectionPool $connectionPool,
    ) {}

    public function getTitlesOfSubpagesAndContent(
        int $parentId,
    ): ?array {
        $connection = $this->connectionPool->getConnectionForTable('pages');
        $unionQueryBuilder = $connection->createQueryBuilder();

        // Passing the outermost QueryBuilder to the subqueries
        $firstPartQueryBuilder = $this->getUnionPart1QueryBuilder($connection, $unionQueryBuilder, $parentId);
        $secondPartQueryBuilder = $this->getUnionPart2QueryBuilder($connection, $unionQueryBuilder, $parentId);

        return $unionQueryBuilder
            ->union($firstPartQueryBuilder)
            ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
            ->orderBy('uid', 'ASC')
            ->executeQuery()
            ->fetchAllAssociative();
    }

    private function getUnionPart1QueryBuilder(
        Connection $connection,
        QueryBuilder $unionQueryBuilder,
        int $pageId,
    ): QueryBuilder {
        $queryBuilder = $connection->createQueryBuilder();
        // The union Expression Builder **must** be used on subqueries
        $unionExpr = $unionQueryBuilder->expr();
        $queryBuilder
            // The column names of the first query are used
            // The column count of both subqueries must be the same
            // The data types must be compatible across columns of the queries
            ->select('title', 'subtitle')
            ->from('pages')
            ->where(
                // The union Expression Builder **must** be used on subqueries
                $unionExpr->eq(
                    'pages.pid',
                    // Named parameters **must** be created on the outermost (union) query builder
                    $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
                ),
            );
        return $queryBuilder;
    }

    private function getUnionPart2QueryBuilder(
        Connection $connection,
        QueryBuilder $unionQueryBuilder,
        int $pageId,
    ): QueryBuilder {
        $queryBuilder = $connection->createQueryBuilder();
        // The union Expression Builder **must** be used on subqueries
        $unionExpr = $unionQueryBuilder->expr();
        $queryBuilder
            // The column count of both subqueries must be the same
            ->select('header', 'subheader')
            ->from('tt_content')
            ->where(
                $unionExpr->eq(
                    'tt_content.pid',
                    // Named parameters **must** be created on the outermost (union) query builder
                    $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT),
                ),
            );
        return $queryBuilder;
    }
}
Copied!
Line 18
All query parts must share the same connection.
Line 19
The outer most QueryBuilder is responsible for the union, it must be used to create named parameters and build expressions within the sub queries.
Line 22-23
We therefore pass the central QueryBuilder responsible for the UNION to all subqueries. Same with the ExpressionBuilder.
Line 25-30
We start building the union() on the first sub query, then add the second sub query using addUnion()
Line 41
Only use the ExpressionBuilder of the sql:UNION within the subqueries.
Line 50
Named parameters must also be called on the outer most union query builder.

The Default Restrictions are applied to each subquery automatically.

setMaxResults() and setFirstResult() 

Add LIMIT to restrict the number of records and OFFSET for pagination of query parts. Both methods should be called only once per statement:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `sys_language` LIMIT 2 OFFSET 4
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language')
    ->setMaxResults(2)
    ->setFirstResult(4)
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • It is allowed to call ->setMaxResults() without calling ->setFirstResult().
  • It is possible to call ->setFirstResult() without calling setMaxResults(): This is equivalent to "Fetch everything, but leave out the first n records". Internally, LIMIT will be added by Doctrine DBAL and set to a very high value.
  • ->setMaxResults(null) can be used to retrieve all results. If an unlimited result set is needed, and no reset of previous instructions is required, this method call should best be omitted for best compatibility.

Changed in version 13.0

Starting with TYPO3 13 null instead of argument 0 (integer) must be used in ->setMaxResults() to return the complete result set without any LIMIT.

add() 

Changed in version 13.0

With the upgrade to Doctrine DBAL version 4 this method has been removed.

Migration: use the direct methods instead:

Before After
->add('select', $array) ->select(...$array)
->add('where', $constraints) ->where(...$constraints)
->add('having', $havings) ->having(...$havings)
->add('orderBy', $orderBy) ->orderBy($orderByField, $orderByDirection)->addOrderBy($orderByField2)
->add('groupBy', $groupBy) ->groupBy($groupField)->addGroupBy($groupField2)

getSQL() 

The ->getSQL() method returns the created query statement as string. It is incredibly useful during development to verify that the final statement is executed exactly as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getSQL());
$result = $queryBuilder->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Casting a query builder object to (string) has the same effect as calling ->getSQL(), but the explicit call using the method should be preferred to simplify a search operation for this kind of debugging statements.
  • The method is a simple way to see what restrictions the RestrictionBuilder has added.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getSQL() does not show these values, instead it displays the placeholder names, usually with a string like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getParameters to see the array of parameters used to replace these placeholders within the query. On the frontend, the queries and parameters are available in the admin panel.

getParameters() 

The ->getParameters() method returns the values for the placeholders of the prepared statement in an array. This is incredibly useful during development to verify that the final statement is executed as a developer expects:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language');
debug($queryBuilder->getParameters());
$statement = $queryBuilder->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • This is debugging code. Take proper actions to ensure that these calls do not end up in production!
  • The method is usually called directly before ->executeQuery() or ->executeStatement() to output the final statement.
  • Doctrine DBAL always creates prepared statements: Each value added via createNamedParameter creates a placeholder that is later replaced when the real query is triggered via ->executeQuery() or ->executeStatement(). ->getParameters() does not show the statement or the placeholders, instead the values are displayed, usually an array using keys like :dcValue1. There is no simple solution to show the fully replaced query within the framework, but you can use getSql to see the string with placeholders, which is used as a prepared statement.

executeQuery() and executeStatement() 

Changed in version 13.0

The ->execute() method has been removed. Use

  • ->executeQuery() returning a \Doctrine\DBAL\Result instead of a \Doctrine\DBAL\Statement (like the ->execute() method returned) and
  • ->executeStatement() returning the number of affected rows.

executeQuery() 

This method compiles and fires the final query statement. This is usually the last call on a query builder object. It can be called for SELECT and COUNT queries.

On success, it returns a result object of type \Doctrine\DBAL\Result representing the result set. The Result object can then be used by fetchAssociative(), fetchAllAssociative() and fetchOne(). executeQuery() returns a \Doctrine\DBAL\Result and not a \Doctrine\DBAL\Statement anymore.

If the query fails for some reason (for instance, if the database connection was lost or if the query contains a syntax error), an \Doctrine\DBAL\Exception is thrown. It is usually bad habit to catch and suppress this exception, as it indicates a runtime error a program error. Both should bubble up. For more information on proper exception handling, see the coding guidelines.

executeStatement() 

The executeStatement() method can be used for INSERT, UPDATE and DELETE statements. It returns the number of affected rows as an integer.

expr() 

This method returns an instance of the ExpressionBuilder. It is used to create complex WHERE query parts and JOIN expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT `uid` FROM `tt_content` WHERE (`uid` > 42)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->gt(
            'uid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Remarks:

  • This object is stateless and can be called and worked on as often as needed. However, it is bound to the specific connection for which a statement is created and therefore only available through the query builder, which is specific to a connection.
  • Never reuse the ExpressionBuilder, especially not between multiple query builder objects, but always get an instance of the expression builder by calling ->expr().

createNamedParameter() 

Changed in version 13.0

Doctrine DBAL v4 dropped the support for using the \PDO::PARAM_* constants in favor of the enum types. Be aware of this and use \TYPO3\CMS\Core\Database\Connection::PARAM_*, which can already be used in TYPO3 v12 and v11.

This method creates a placeholder for a field value of a prepared statement. Always use this when dealing with user input in expressions to protect the statement from SQL injections:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT * FROM `tt_content` WHERE (`bodytext` = 'kl\'aus')
$searchWord = "kl'aus"; // $searchWord retrieved from the PSR-7 request
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord, Connection::PARAM_STR)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

The above example shows the importance of using ->createNamedParameter(): The search word kl'aus is "tainted" and would break the query if not channeled through ->createNamedParameter(), which quotes the backtick and makes the value SQL injection-safe.

Not convinced? Suppose the code would look like this:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// NEVER EVER DO THIS!
$_POST['searchword'] = "'foo' UNION SELECT username FROM be_users";
$searchWord = $request->getParsedBody()['searchword']);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
 this fails with syntax error to prevent copy and paste
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        // MASSIVE SECURITY ISSUE DEMONSTRATED HERE
        // USE ->createNamedParameter() ON $searchWord!
        $queryBuilder->expr()->eq('bodytext', $searchWord)
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Mind the missing ->createNamedParameter() method call in the ->eq() expression for a given value! This code would happily execute the statement SELECT uid FROM `tt_content` WHERE `bodytext` = 'foo' UNION SELECT username FROM be_users; returning a list of backend user names!

More examples 

Use integer, integer array:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// SELECT * FROM `tt_content`
//     WHERE `bodytext` = 'kl\'aus'
//     AND   sys_language_uid = 0
//     AND   pid in (2, 42,13333)
$searchWord = "kl'aus"; // $searchWord retrieved from the PSR-7 request
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord)
        ),
        $queryBuilder->expr()->eq(
            'sys_language_uid',
            $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)
        ),
        $queryBuilder->expr()->in(
            'pid',
            $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
        )
    )
    ->executeQuery();
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Rules 

  • Always use ->createNamedParameter() for any input, no matter where it comes from.
  • The second argument of ->expr() is always either a call to ->createNamedParameter() or ->quoteIdentifier().
  • The second argument of ->createNamedParameter() specifies the type of input. For string, this can be omitted, but it is good practice to add \TYPO3\CMS\Core\Database\Connection::PARAM_INT for integers or similar for other field types. This is not strict rule currently, but if you follow it you will have fewer headaches in the future, especially with DBMSes that are not as relaxed as MySQL when it comes to field types. The Connection constants can be used for simple types like bool, string, null, lob and integer. Additionally, the two constants Connection::PARAM_INT_ARRAY and Connection::PARAM_STR_ARRAY can be used when handling an array of strings or integers, for instance in an IN() expression.
  • Keep the ->createNamedParameter() method as close to the expression as possible. Do not structure your code in a way that it quotes something first and only later stuffs the already prepared names into the expression. Having ->createNamedParameter() directly within the created expression, is much less error-prone and easier to review. This is a general rule: Sanitizing input must be done as close as possible to the "sink" where a value is passed to a lower part of the framework. This paradigm should also be followed for other quote operations like htmlspecialchars() or GeneralUtility::quoteJSvalue(). Sanitization should be obvious directly at the very place where it is important:
EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// DO
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'bodytext',
            $queryBuilder->createNamedParameter($searchWord, Connection::PARAM_STR)
        )
    )

// DON'T DO, this is much harder to track:
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$myValue = $queryBuilder->createNamedParameter($searchWord);
// Imagine much more code here
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq('bodytext', $myValue)
    )
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

quoteIdentifier() and quoteIdentifiers() 

->quoteIdentifier() must be used when not a value but a field name is handled. The quoting is different in those cases and typically ends up with backticks ` instead of ticks ':

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// SELECT `uid` FROM `tt_content` WHERE (`header` = `bodytext`)
// Return list of rows where header and bodytext values are identical
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'header',
            $queryBuilder->quoteIdentifier('bodytext')
        )
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool.

The method quotes single field names or combinations of table names or table aliases with field names:

Some quote examples
// Single field name: `bodytext`
->quoteIdentifier('bodytext');

// Table name and field name: `tt_content`.`bodytext`
->quoteIdentifier('tt_content.bodytext')

// Table alias and field name: `foo`.`bodytext`
->from('tt_content', 'foo')->quoteIdentifier('foo.bodytext')
Copied!

Remarks:

  • Similar to ->createNamedParameter() this method is crucial to prevent SQL injections. The same rules apply here.
  • The ->set() method for UPDATE statements expects its second argument to be a field value by default, and quotes it internally using ->createNamedParameter(). If a field should be set to the value of another field, this quoting can be turned off and an explicit call to ->quoteIdentifier() must be added.
  • Internally, ->quoteIdentifier() is automatically called on all method arguments that must be a field name. For instance, ->quoteIdentifier() is called for all arguments of ->select().
  • ->quoteIdentifiers() (mind the plural) can be used to quote multiple field names at once. While that method is "public" and thus exposed as an API method, this is mostly useful internally only.

escapeLikeWildcards() 

Helper method to quote % characters within a search string. This is helpful in ->like() and ->notLike() expressions:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// SELECT `uid` FROM `tt_content` WHERE (`bodytext` LIKE '%kl\\%aus%')
$searchWord = 'kl%aus';
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->select('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->like(
            'bodytext',
            $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($searchWord) . '%', Connection::PARAM_STR)
        )
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

getRestrictions(), setRestrictions(), resetRestrictions() 

API methods to deal with the RestrictionBuilder.

Connection 

Introduction 

The \TYPO3\CMS\Core\Database\Connection class extends the basic Doctrine DBAL \Doctrine\DBAL\Connection class and is mainly used internally in TYPO3 to establish, maintain and terminate connections to single database endpoints. These internal methods are not the scope of this documentation, since an extension developer usually does not have to deal with them.

However, for an extension developer, the class provides a list of short-hand methods that allow you to deal with query cases without the complexity of the query builder. Using these methods usually ends up in rather short and easy-to-read code. The methods have in common that they only support "equal" comparisons in WHERE conditions, that all fields and values are automatically fully quoted, and that the created queries are executed right away.

Instantiation 

Using the connection pool 

An instance of the \TYPO3\CMS\Core\Database\Connection class is retrieved from the ConnectionPool by calling ->getConnectionForTable() and passing the table name for which a query should be executed. The ConnectionPool can be injected via constructor:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_domain_model_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
    }
}
Copied!

Via dependency injection 

Another way is to inject the Connection object directly via dependency injection if you only use one table.

  1. Configure the concrete connection as a service

    To make a concrete Connection object available as a service, use the factory option in the service configuration:

    EXT:my_extension/Configuration/Services.yaml
    services:
      _defaults:
        autowire: true
        autoconfigure: true
        public: false
    
      MyVendor\MyExtension\:
        resource: '../Classes/*'
    
      connection.tx_myextension_domain_model_mytable:
        class: 'TYPO3\CMS\Core\Database\Connection'
        factory: ['@TYPO3\CMS\Core\Database\ConnectionPool', 'getConnectionForTable']
        arguments:
          - 'tx_myextension_domain_model_mytable'
    
      MyVendor\MyExtension\Domain\Repository\MyTableRepository:
        arguments:
          - '@connection.tx_myextension_domain_model_mytable'
    
    Copied!
  2. Use constructor injection in your class

    Now the Connection object for a specific table can be injected via the constructor:

    EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Domain\Repository;
    
    use TYPO3\CMS\Core\Database\Connection;
    
    final class MyTableRepository
    {
        public function __construct(
            private readonly Connection $connection,
        ) {}
    
        public function findSomething()
        {
            // Here you can use $this->connection directly
        }
    }
    
    Copied!

Parameter types 

The parameter types are used in various places to bind values to types, for example, when using named parameters in the query builder:

// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder->createNamedParameter(42, Connection::PARAM_INT);
Copied!

The following parameter types are available:

\TYPO3\CMS\Core\Database\Connection::PARAM_NULL
Represents an SQL NULL data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_INT
Represents an SQL INTEGER data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_STR
Represents an SQL CHAR or VARCHAR data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_LOB
Represents an SQL large object data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_BOOL
Represents a boolean data type.
\TYPO3\CMS\Core\Database\Connection::PARAM_INT_ARRAY
Represents an array of integer values.
\TYPO3\CMS\Core\Database\Connection::PARAM_STR_ARRAY
Represents an array of string values.

The default parameter type is Connection::PARAM_STR, if this argument is omitted.

Internally, these parameter types are mapped to the types Doctrine DBAL expects.

insert() 

The insert() method creates and executes an INSERT INTO statement. Example:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomething(
        int $pid,
        string $someString,
        array $someData,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->insert(
                self::TABLE_NAME,
                [
                    'pid' => $pid,
                    'some_string' => $someString,
                    'json_field' => $someData,
                ],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

New in version 12.1

Arguments of the insert() method:

  1. The name of the table the row should be inserted. Required.
  2. An associative array containing field/value pairs. The key is a field name, the value is the value to be inserted. All keys are quoted to field names and all values are quoted to string values. Required.
  3. Specify how single values are quoted. This is useful if a date, number or similar should be inserted. Optional.

    The example below quotes the first value to an integer and the second one to a string:

    EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Domain\Repository;
    
    use TYPO3\CMS\Core\Database\Connection;
    use TYPO3\CMS\Core\Database\ConnectionPool;
    
    final class MyTableRepository
    {
        private const TABLE_NAME = 'tx_myextension_mytable';
    
        public function __construct(
            private readonly ConnectionPool $connectionPool,
        ) {}
    
        public function insertSomething(
            int $pid,
            string $someString,
        ): void {
            $this->connectionPool
                ->getConnectionForTable(self::TABLE_NAME)
                ->insert(
                    self::TABLE_NAME,
                    [
                        'pid' => $pid,
                        'some_string' => $someString,
                    ],
                    [
                        Connection::PARAM_INT,
                        Connection::PARAM_STR,
                    ],
                );
        }
    }
    
    Copied!

    Read how to instantiate a connection with the connection pool. See available parameter types.

insert() returns the number of affected rows. Guess what? That is the number 1 ... If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

bulkInsert() 

This method insert multiple rows at once:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function bulkInsertSomething(
        int $pid1,
        int $pid2,
        string $someString1,
        string $someString2,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->bulkInsert(
                self::TABLE_NAME,
                [
                    [$pid1, $someString1],
                    [$pid2, $someString2],
                ],
                [
                    'pid',
                    'title',
                ],
                [
                    Connection::PARAM_INT,
                    Connection::PARAM_STR,
                ],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

Arguments of the bulkInsert() method:

  1. The name of the table the row should be inserted. Required.
  2. An array of the values to be inserted. Required.
  3. An array containing the column names of the data which should be inserted. Optional.
  4. Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted to strings. Optional.

The number of inserted rows are returned. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

update() 

Create an UPDATE statement and execute it. The example from FAL's ResourceStorage sets a storage to offline:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function updateSomething(
        int $uid,
        string $someValue,
        array $someData,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->update(
                self::TABLE_NAME,
                [
                    'some_value' => $someValue,
                    'json_data' => $someData,
                ],
                ['uid' => $uid],
                [Connection::PARAM_INT],
            );
    }
}
Copied!

New in version 12.1

Read how to instantiate a connection with the connection pool. See available parameter types.

Arguments of the update() method:

  1. The name of the table to update. Required.
  2. An associative array containing field/value pairs to be updated. The key is a field name, the value is the value. In SQL they are mapped to the SET keyword. Required.
  3. The update criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.
  4. Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted to strings. Optional.

The method returns the number of updated rows. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

delete() 

Execute a DELETE query using equal conditions in WHERE, example from BackendUtility, to mark rows as no longer locked by a user:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function deleteSomething(
        int $uid,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->delete(
                self::TABLE_NAME,
                ['uid' => $uid],
                [Connection::PARAM_INT],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool. See available parameter types.

Arguments of the delete() method:

  1. The name of the table. Required.
  2. The delete criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.
  3. Specify how single values are quoted. Similar to insert(); if omitted, everything will be quoted to strings. Optional.

The method returns the number of deleted rows. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

truncate() 

This method empties a table, removing all rows. It is usually much faster than a delete() of all rows. This typically resets "auto increment primary keys" to zero. Use with care:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyCacheRepository
{
    private const TABLE_NAME = 'cache_myextension';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function truncateSomething(
        int $uid,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->truncate(self::TABLE_NAME);
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

The argument is the name of the table to be truncated. If something goes wrong, a \Doctrine\DBAL\Exception is thrown.

count() 

This method executes a COUNT query. Again, this becomes useful when very simple COUNT statements are to be executed. The example below returns the number of active rows (not hidden or deleted or disabled by time) from the table tx_myextension_mytable whose field some_value field set to $something:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function countSomething(
        int $something,
    ): int {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
        return $connection->count(
            '*',
            self::TABLE_NAME,
            ['some_value' => $something],
        );
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Arguments of the count() method:

  1. The field to count, usually * or uid. Required.
  2. The name of the table. Required.
  3. The select criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Required.

The method returns the counted rows.

Remarks:

  • Connection::count() returns the number directly as an integer, unlike the method of the query builder it is not necessary to call ->fetchColumns(0) or similar.
  • The third argument expects all WHERE values to be strings, each single expression is combined with AND.
  • The restriction builder kicks in and adds additional WHERE conditions based on TCA settings.
  • Field names and values are quoted automatically.
  • If anything more complex than a simple equal condition on WHERE is needed, the query builder methods are the better choice: next to select(), the ->count() query is often the least useful method of the Connection object.

select() 

This method creates and executes a simple SELECT query based on equal conditions. Its usage is limited, the restriction builder kicks in and key/value pairs are automatically quoted:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use Doctrine\DBAL\Result;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function countSomething(
        int $something,
    ): Result {
        return $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->select(
                ['*'],
                self::TABLE_NAME,
                ['some_value' => $something],
            );
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Arguments of the select() method:

  1. The columns of the table which to select as an array. Required.
  2. The name of the table. Required.
  3. The select criteria as an array of key/value pairs. The key is the field name, the value is the value. In SQL they are mapped in a WHERE keyword combined with AND. Optional.
  4. The columns to group the results by as an array. In SQL they are mapped in a GROUP BY keyword. Optional.
  5. An associative array of column name/sort directions pairs. In SQL they are mapped in an ORDER BY keyword. Optional.
  6. The maximum number of rows to return. In SQL it is mapped in a LIMIT keyword. Optional.
  7. The first result row to select (when used the maximum number of rows). In SQL it is mapped in an OFFSET keyword. Optional.

In contrast to the other short-hand methods, ->select() returns a Result object ready for ->fetchAssociative() to get single rows or for ->fetchAllAssociative() to get all rows at once.

Remarks:

  • For non-trivial SELECT queries it is often better to switch to the according method of the query builder object.
  • The restriction builder adds default WHERE restrictions. If these restrictions do not match the query requirements, it is necessary to switch to the QueryBuilder->select() method for fine-grained WHERE manipulation.

lastInsertId() 

Changed in version 13.0

The method no longer accepts the table name as first argument and the name of the auto-increment field as second argument.

This method returns the uid of the last insert() statement. This is useful if the ID is to be used directly afterwards:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomething(
        array $someData,
    ): int {
        $connection = $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME);
        $connection
            ->insert(
                self::TABLE_NAME,
                $someData,
            );
        return (int)$connection->lastInsertId();
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Remarks:

  • The last inserted ID needs to be retrieved directly before inserting a record to another table. That should be the usual workflow used in the wild - but be aware of this.

createQueryBuilder() 

The query builder should not be reused for multiple different queries. However, sometimes it is convenient to first fetch a connection object for a specific table and execute a simple query, and later create a query builder for a more complex query from that connection object. The usefulness of this method is limited, however, and at the time of writing no good example could be found in the Core.

The method can also be useful in loops to save some precious code characters:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function useQueryBuilder(
        array $someData,
    ): void {
        $connection = $this->connectionPool->getConnectionForTable(self::TABLE_NAME);
        foreach ($someData as $value) {
            $queryBuilder = $connection->createQueryBuilder();
            $myResult = $queryBuilder
                ->select('*')
                ->from(self::TABLE_NAME)
                ->where(
                    $queryBuilder->expr()->eq(
                        'some_field',
                        $queryBuilder->createNamedParameter($value),
                    ),
                )
                ->executeQuery()
                ->fetchAllAssociative();
            // do something
        }
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Native JSON database field type support 

New in version 12.1

TYPO3 Core's Database API based on Doctrine DBAL supports the native database field type json, which is already available for all supported DBMS of TYPO3 v12.

JSON-like objects or arrays are automatically serialized during writing a dataset to the database, when the native JSON type was used in the database schema definition.

By using the native database field declaration json in ext_tables.sql file within an extension, TYPO3 converts arrays or objects of type \JsonSerializable into a serialized JSON value in the database when persisting such values via Connection->insert() or Connection->update() if no explicit DB types are handed in as additional method argument.

TYPO3 now utilizes the native type mapping of Doctrine to convert special types such as JSON database field types automatically for writing.

Example:

EXT:my_extension/ext_tables.sql
CREATE TABLE tx_myextension_mytable
(
    some_string varchar(200) DEFAULT '',
    json_field  json
);
Copied!
EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_mytable';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function insertSomething(
        int $pid,
        string $someString,
        array $someData,
    ): void {
        $this->connectionPool
            ->getConnectionForTable(self::TABLE_NAME)
            ->insert(
                self::TABLE_NAME,
                [
                    'pid' => $pid,
                    'some_string' => $someString,
                    'json_field' => $someData,
                ],
            );
    }
}
Copied!

Expression builder 

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder

The ExpressionBuilder class is responsible for dynamically creating parts of SQL queries.

It builds query conditions, ensuring that table and column names are quoted inside the expressions / SQL fragments. The class is a facade to the Doctrine ExpressionBuilder.

The ExpressionBuilder is used in the context of the QueryBuilder to ensure that queries conform to the requirements of whichever database platform is in use.

Basic usage 

An instance of the ExpressionBuilder is retrieved from the QueryBuilder object:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
$expressionBuilder = $queryBuilder->expr();
Copied!

It is good practice not to assign an instance of the ExpressionBuilder to a variable, but to use it directly within the code flow of the query builder context:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';

    public function __construct(
        private readonly ConnectionPool $connectionPool,
    ) {}

    public function findSomething()
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);

        $rows = $queryBuilder
            ->select('uid', 'header', 'bodytext')
            ->from(self::TABLE_NAME)
            ->where(
                // `bodytext` = 'lorem' AND `header` = 'dolor'
                $queryBuilder->expr()->eq(
                    'bodytext',
                    $queryBuilder->createNamedParameter('lorem', Connection::PARAM_STR),
                ),
                $queryBuilder->expr()->eq(
                    'header',
                    $queryBuilder->createNamedParameter('dolor', Connection::PARAM_STR),
                ),
            )
            ->executeQuery()
            ->fetchAllAssociative();

        // ...
    }
}
Copied!

See available parameter types.

Junctions 

  • ->and() conjunction
  • ->or() disjunction

Combine multiple single expressions using AND or OR. Nesting is possible, both methods are variadic and accept any number of arguments, which are then all combined. It usually makes little sense to pass zero or only one argument.

Example: finding tt_content records:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function findSomething(): QueryBuilder
    {
        // WHERE
        //     (`tt_content`.`CType` = 'header')
        //     AND (
        //        (`tt_content`.`header_position` = 'center')
        //        OR
        //        (`tt_content`.`header_position` = 'right')
        //     )
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $queryBuilder->where(
            $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('header')),
            $queryBuilder->expr()->or(
                $queryBuilder->expr()->eq(
                    'header_position',
                    $queryBuilder->createNamedParameter('center', Connection::PARAM_STR),
                ),
                $queryBuilder->expr()->eq(
                    'header_position',
                    $queryBuilder->createNamedParameter('right', Connection::PARAM_STR),
                ),
            ),
        );
        return $queryBuilder;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

Comparisons 

A set of methods to create comparison expressions and SQL functions:

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
eq ( string $fieldName, ?mixed $value)

Creates an equality comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
neq ( string $fieldName, ?mixed $value)

Creates a non equality comparison expression with the given arguments.

First argument is considered the left expression and the second is the right expression. When converted to string, it will generate a <left expr> <> <right expr>. Example:

[php]
// u.id <> 1
$q->where($q->expr()->neq('u.id', '1'));
Copied!
param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
lt ( string $fieldName, ?mixed $value)

Creates a lower-than comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
lte ( string $fieldName, ?mixed $value)

Creates a lower-than-equal comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
gt ( string $fieldName, ?mixed $value)

Creates a greater-than comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
gte ( string $fieldName, ?mixed $value)

Creates a greater-than-equal comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The value. No automatic quoting/escaping is done.

Returns
string
isNull ( string $fieldName)

Creates an IS NULL expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

Returns
string
isNotNull ( string $fieldName)

Creates an IS NOT NULL expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

Returns
string
like ( string $fieldName, ?mixed $value, ?string $escapeChar = NULL)

Creates a LIKE() comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in LIKE() comparison. No automatic quoting/escaping is done.

param $escapeChar

the escapeChar, default: NULL

Returns
string
notLike ( string $fieldName, ?mixed $value, ?string $escapeChar = NULL)

Creates a NOT LIKE() comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in NOT LIKE() comparison. No automatic quoting/escaping is done.

param $escapeChar

the escapeChar, default: NULL

Returns
string
in ( string $fieldName, ?string|array $value)

Creates an IN () comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The placeholder or the array of values to be used by IN() comparison.No automatic quoting/escaping is done.

Returns
string
notIn ( string $fieldName, ?string|array $value)

Creates a NOT IN () comparison expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

The placeholder or the array of values to be used by NOT IN() comparison.No automatic quoting/escaping is done.

Returns
string
inSet ( string $fieldName, string $value, bool $isColumn = false)

Returns a comparison that can find a value in a list field (CSV).

param $fieldName

The field name. Will be quoted according to database platform automatically.

param $value

Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.

param $isColumn

Set when the value to compare is a column on a table to activate casting, default: false

Returns
string
notInSet ( string $fieldName, string $value, bool $isColumn = false)

Returns a comparison that can find a value in a list field (CSV) but is negated.

param $fieldName

The field name. Will be quoted according to database platform automatically.

param $value

Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.

param $isColumn

Set when the value to compare is a column on a table to activate casting, default: false

Returns
string
bitAnd ( string $fieldName, int $value)

Creates a bitwise AND expression with the given arguments.

param $fieldName

The fieldname. Will be quoted according to database platform automatically.

param $value

Argument to be used in the bitwise AND operation

Returns
string

Remarks and warnings:

Examples:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
// use TYPO3\CMS\Core\Database\Connection;

// `bodytext` = 'foo' - string comparison
->eq('bodytext', $queryBuilder->createNamedParameter('foo'))

// `tt_content`.`bodytext` = 'foo'
->eq('tt_content.bodytext', $queryBuilder->createNamedParameter('foo'))

// `aTableAlias`.`bodytext` = 'foo'
->eq('aTableAlias.bodytext', $queryBuilder->createNamedParameter('foo'))

// `uid` = 42 - integer comparison
->eq('uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))

// `uid` >= 42
->gte('uid', $queryBuilder->createNamedParameter(42, Connection::PARAM_INT))

// `bodytext` LIKE 'lorem'
->like(
    'bodytext',
    $queryBuilder->createNamedParameter(
        $queryBuilder->escapeLikeWildcards('lorem')
    )
)

// `bodytext` LIKE '%lorem%'
->like(
    'bodytext',
    $queryBuilder->createNamedParameter(
        '%' . $queryBuilder->escapeLikeWildcards('lorem') . '%'
    )
)

// usergroup does not contain 42
->notInSet('usergroup', $queryBuilder->createNamedParameter('42'))

// use TYPO3\CMS\Core\Database\Connection;
// `uid` IN (42, 0, 44) - properly sanitized, mind the intExplode and PARAM_INT_ARRAY
->in(
    'uid',
    $queryBuilder->createNamedParameter(
        GeneralUtility::intExplode(',', '42, karl, 44', true),
        Connection::PARAM_INT_ARRAY
    )
)

// use TYPO3\CMS\Core\Database\Connection;
// `CType` IN ('media', 'multimedia') - properly sanitized, mind the PARAM_STR_ARRAY
->in(
    'CType',
    $queryBuilder->createNamedParameter(
        ['media', 'multimedia'],
        Connection::PARAM_STR_ARRAY
    )
)
Copied!

See available parameter types.

Aggregate functions 

Aggregate functions used in SELECT parts, often combined with GROUP BY. The first argument is the field name (or table name / alias with field name), the second argument is an optional alias.

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
min ( string $fieldName, ?string $alias = NULL)

Creates a MIN expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
max ( string $fieldName, ?string $alias = NULL)

Creates a MAX expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
avg ( string $fieldName, ?string $alias = NULL)

Creates an AVG expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
sum ( string $fieldName, ?string $alias = NULL)

Creates a SUM expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string
count ( string $fieldName, ?string $alias = NULL)

Creates a COUNT expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string

Examples:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use Doctrine\DBAL\Exception;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    /**
     * Calculate the average creation timestamp of all rows from tt_content
     * SELECT AVG(`crdate`) AS `averagecreation` FROM `tt_content`
     * @return array<mixed>
     * @throws Exception
     */
    public function findAverageCreationTime(): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $result = $queryBuilder
            ->addSelectLiteral(
                $queryBuilder->expr()->avg('crdate', 'averagecreation'),
            )
            ->from(self::TABLE_NAME)
            ->executeQuery()
            ->fetchAssociative();
        return $result;
    }

    /**
     * Distinct list of all existing endtime values from tt_content
     * SELECT `uid`, MAX(`endtime`) AS `maxendtime` FROM `tt_content` GROUP BY `endtime`
     * @return array<array<mixed>>
     * @throws Exception
     */
    public function findDistinctiveEndtimeValues(): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $result = $queryBuilder
            ->select('uid')
            ->addSelectLiteral(
                $queryBuilder->expr()->max('endtime', 'maxendtime'),
            )
            ->from('tt_content')
            ->groupBy('endtime')
            ->executeQuery()
            ->fetchAllAssociative()
        ;
        return $result;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Various expressions 

ExpressionBuilder::as() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
as ( string $expression, string $asIdentifier = '')
param $expression

Value, identifier or expression which should be aliased

param $asIdentifier

Alias identifier, default: ''

Return description

Returns aliased expression

Returns
string

Creates a statement to append a field alias to a value, identifier or sub-expression.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tx_myextension_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateExpressionBuilderAs(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        $expressionBuilder = $queryBuilder->expr();

        // Alias the result of "1+1+1" as column "calculated_field" (containing "3")
        $queryBuilder->selectLiteral(
            $queryBuilder->quoteIdentifier('uid'),
            $expressionBuilder->as('(1 + 1 + 1)', 'calculated_field'),
        );

        // Alias a calculated sub-expression of concatenating "1", " " and "1" as
        // column "concatenated_value", containing "1 1".
        $queryBuilder->selectLiteral(
            $queryBuilder->quoteIdentifier('uid'),
            $expressionBuilder->as(
                $expressionBuilder->concat(
                    $expressionBuilder->literal('1'),
                    $expressionBuilder->literal(' '),
                    $expressionBuilder->literal('1'),
                ),
                'concatenated_value',
            ),
        );
    }
}
Copied!

ExpressionBuilder::concat() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
concat ( string ...$parts)
param $parts

the parts

Return description

Returns the concatenation expression compatible with the database connection platform

Returns
string

Can be used to concatenate values, row field values or expression results into a single string value.

The resulting value is built using a platform-specific and preferred concatenation method, for example field1 || field2 || field3 || ... for SQLite and CONCAT(field1, field2, field3, ...) for other database vendors.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateConcat(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        $expressionBuilder = $queryBuilder->expr();
        $result = $queryBuilder
            ->select('uid', 'pid', 'title', 'page_title_info')
            ->addSelectLiteral(
                $expressionBuilder->concat(
                    $queryBuilder->quoteIdentifier('title'),
                    $queryBuilder->quote(' - ['),
                    $queryBuilder->quoteIdentifier('uid'),
                    $queryBuilder->quote('|'),
                    $queryBuilder->quoteIdentifier('pid'),
                    $queryBuilder->quote(']'),
                ) . ' AS ' . $queryBuilder->quoteIdentifier('page_title_info'),
            )
            ->where(
                $expressionBuilder->eq(
                    'pid',
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT),
                ),
            )
            ->executeQuery();
        while ($row = $result->fetchAssociative()) {
            // $row = [
            //  'uid' => 1,
            //  'pid' => 0,
            //  'title' => 'Site Root Page',
            //  'page_title_info' => 'Site Root Page - [1|0]',
            // ]
            // ...
        }
    }
}
Copied!

ExpressionBuilder::castInt() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
castInt ( string $value, string $asIdentifier = '')
param $value

Quoted value or expression result which should be cast to integer type

param $asIdentifier

Optionally add a field identifier alias (AS), default: ''

Return description

Returns the integer cast expression compatible with the connection database platform

Returns
string

Can be used to create an expression which converts a value, row field value or the result of an expression to a signed integer type.

Uses the platform-specific preferred way for casting to dynamic length character type, which means CAST("value" AS INTEGER) for most database vendors except PostgreSQL. For PostgreSQL "value"::INTEGER cast notation is used.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastInt(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        $queryBuilder
            ->select('uid')
            ->from('pages');

        // simple value (quoted) to be used as sub-expression
        $expression1 = $queryBuilder->expr()->castInt(
            $queryBuilder->quote('123'),
        );

        // simple value (quoted) to return as select field
        $queryBuilder->addSelectLiteral(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quote('123'),
                'virtual_field',
            ),
        );

        // cast the contents of a specific field to integer
        $expression3 = $queryBuilder->expr()->castInt(
            $queryBuilder->quoteIdentifier('uid'),
        );

        // expression to be used as sub-expression
        $expression4 = $queryBuilder->expr()->castInt(
            $queryBuilder->expr()->castVarchar('(1 * 10)'),
        );

        // expression to return as select field
        $queryBuilder->addSelectLiteral(
            $queryBuilder->expr()->castInt(
                $queryBuilder->expr()->castVarchar('(1 * 10)'),
                'virtual_field',
            ),
        );
    }
}
Copied!

ExpressionBuilder::castText() 

New in version 13.3

Can be used to create an expression which converts a value, row field value or the result of an expression to type TEXT or a large `VARCHAR, depending on which database system is in use.

Casting is done to large VARCHAR/CHAR types using the CAST/CONVERT or similar methods based on the database engine.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';

    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastText(string $col1, string $col2): array
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        return $queryBuilder
            ->selectLiteral(
                $queryBuilder->quoteIdentifier($col1),
                $queryBuilder->quoteIdentifier($col2),
                // !!! Escape all values passed to castText to prevent SQL injections
                $queryBuilder->expr()->castText(
                    $queryBuilder->quoteIdentifier($col2) . ' * 10',
                    'virtual_field',
                ),
            )
            ->from(self::TABLE_NAME)
            ->executeQuery()
            ->fetchAllAssociative();
    }
}
Copied!

ExpressionBuilder::castVarchar() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
castVarchar ( string $value, int $length = 255, string $asIdentifier = '')
param $value

Unquoted value or expression, which should be cast

param $length

Dynamic varchar field length, default: 255

param $asIdentifier

Used to add a field identifier alias (AS) if non-empty string (optional), default: ''

Return description

Returns the cast expression compatible for the database platform

Returns
string

Can be used to create an expression which converts a value, row field value or the result of an expression to a varchar type with dynamic length.

Uses the platform-specific preferred way for casting to dynamic length character type, which means CAST("value" AS VARCHAR(<LENGTH>)) or CAST("value" AS CHAR(<LENGTH>)), except for PostgreSQL where "value"::INTEGER cast notation is used.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'my_table';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateCastVarchar(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);

        $fieldVarcharCastExpression = $queryBuilder->expr()->castVarchar(
            $queryBuilder->quote('123'), // integer as string
            255,                         // convert to varchar(255) field - dynamic length
            'new_field_identifier',
        );

        $fieldExpressionCastExpression2 = $queryBuilder->expr()->castVarchar(
            '(100 + 200)',           // calculate a integer value
            100,                     // dynamic varchar(100) field
            'new_field_identifier',
        );
    }
}
Copied!

ExpressionBuilder::if() 

New in version 13.3

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
if ( TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|Doctrine\DBAL\Query\Expression\CompositeExpression|Stringable|string $condition, Stringable|string $truePart, Stringable|string $falsePart, ?Stringable|string|null $as = NULL)
param $condition

the condition

param $truePart

the truePart

param $falsePart

the falsePart

param $as

the as, default: NULL

Returns
string

This method is used for "if-then-else" expressions. These are translated into IF or CASE statements depending on the database engine in use.

Example:

// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder
    ->selectLiteral(
        $queryBuilder->expr()->if(
            $queryBuilder->expr()->eq(
                'hidden',
                $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
            ),
            $queryBuilder->quote('page-is-visible'),
            $queryBuilder->quote('page-is-not-visible'),
            'result_field_name'
        ),
    )
    ->from('pages');
Copied!

Result with MySQL/MariaDB:

SELECT
    (IF(`hidden` = 0, 'page-is-visible', 'page-is-not-visible')) AS `result_field_name`
    FROM `pages`
Copied!

ExpressionBuilder::left() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
left ( string|int $length, string $value, string $asIdentifier = '')
param $length

Integer value or expression providing the length as integer

param $value

Value, identifier or expression defining the value to extract from the left

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Return the expression to extract defined substring from the right side.

Returns
string

Extract $length characters of $value from the left.

Creates a LEFT("value", number_of_chars) expression for all supported database vendors except SQLite, which uses substring("value", start[, number_of_chars]) to provide a compatible expression.

ExpressionBuilder::leftPad() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
leftPad ( string $value, string|int $length, string $paddingValue, string $asIdentifier = '')
param $value

Value, identifier or expression defining the value which should be left padded

param $length

Padded length, to either fill up with $paddingValue on the left side or crop to

param $paddingValue

Padding character used to fill up if characters are missing on the left side

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns database connection platform compatible left-pad expression.

Returns
string

Left-pad the value or sub-expression result with $paddingValue, to a total length of $length.

SQLite does not support LPAD("value", length, "paddingValue"), therefore a more complex compatible replacement expression construct is created.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateLeftPad(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Left-pad "123" with "0" to an amount of 10 times, resulting in "0000000123"
        $expression1 = $queryBuilder->expr()->leftPad(
            $queryBuilder->quote('123'),
            10,
            '0',
        );

        // Left-pad contents of the "uid" field with "0" to an amount of 10 times, a uid=1 would return "0000000001"
        $expression2 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
            10,
            '0',
        );

        // Sub-expression to left-pad the concated string result ("1" + "2" + "3") up to 10 times with 0, resulting in "0000000123".
        $expression3 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->concat(
                $queryBuilder->quote('1'),
                $queryBuilder->quote('2'),
                $queryBuilder->quote('3'),
            ),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "0000001123".
        $expression4 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "0000001123" being assigned to "virtual_field"
        $expression5 = $queryBuilder->expr()->leftPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
            'virtual_field',
        );
    }
}
Copied!

ExpressionBuilder::length() 

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
length ( string $fieldName, ?string $alias = NULL)

Creates a LENGTH expression for the given field/alias.

param $fieldName

the fieldName

param $alias

the alias, default: NULL

Returns
string

The length() string function can be used to return the length of a string in bytes.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function findFieldLongerThenZero(string $fieldName): QueryBuilder
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
        $queryBuilder->expr()->comparison(
            $queryBuilder->expr()->length($fieldName),
            ExpressionBuilder::GT,
            $queryBuilder->createNamedParameter(0, Connection::PARAM_INT),
        );
        return $queryBuilder;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

ExpressionBuilder::repeat() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
repeat ( string|int $numberOfRepeats, string $value, string $asIdentifier = '')
param $numberOfRepeats

Statement or value defining how often the $value should be repeated. Proper quoting must be ensured.

param $value

Value which should be repeated. Proper quoting must be ensured

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns the platform compatible statement to create the x-times repeated value

Returns
string

Create a statement to generate a value which repears the $value for $numberOfRepeats times. This method can be used to provide the repeat number as a sub-expression or calculation.

REPEAT("value", numberOfRepeats) is used to build this expression for all database vendors except SQLite which uses REPLACE(PRINTF('%.' || <valueOrStatement> || 'c', '/'),'/', <repeatValue>) , based on REPLACE() and the built-in printf().

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRepeat(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Repeats "." 10 times, resulting in ".........."
        $expression1 = $queryBuilder->expr()->repeat(
            10,
            $queryBuilder->quote('.'),
        );

        // Repeats "0" 20 times and allows to access the field as "aliased_field" in query / result
        $expression2 = $queryBuilder->expr()->repeat(
            20,
            $queryBuilder->quote('0'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeat contents of field "table_field" 20 times and makes it available as "aliased_field"
        $expression3 = $queryBuilder->expr()->repeat(
            20,
            $queryBuilder->quoteIdentifier('table_field'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeate database field "table_field" the number of times that is cast to integer from the field "repeat_count_field" and make it available as "aliased_field"
        $expression4 = $queryBuilder->expr()->repeat(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('repeat_count_field'),
            ),
            $queryBuilder->quoteIdentifier('table_field'),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Repeats the character "." as many times as the result of the expression "7+3" (10 times)
        $expression5 = $queryBuilder->expr()->repeat(
            '(7 + 3)',
            $queryBuilder->quote('.'),
        );

        // Repeat 10 times the result of a concatenation expression (".") and make it available as "virtual_field_name"
        $expression6 = $queryBuilder->expr()->repeat(
            '(7 + 3)',
            $queryBuilder->expr()->concat(
                $queryBuilder->quote(''),
                $queryBuilder->quote('.'),
                $queryBuilder->quote(''),
            ),
            'virtual_field_name',
        );
    }
}
Copied!

ExpressionBuilder::right() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
right ( string|int $length, string $value, string $asIdentifier = '')
param $length

Integer value or expression providing the length as integer

param $value

Value, identifier or expression defining the value to extract from the left

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Return the expression to extract defined substring from the right side

Returns
string

Extract $length characters of $value from the right.

Creates a RIGHT("value", length) expression for all supported database vendors except SQLite, which uses substring("value", start_of_string[, length]).

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRight(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Returns the right-side 6 characters of "some-string" (result: "string")
        $expression1 = $queryBuilder->expr()->right(
            6,
            $queryBuilder->quote('some-string'),
        );

        // Returns the right-side calculated 7 characters of "some-string" (result: "-string")
        $expression2 = $queryBuilder->expr()->right(
            '(3+4)',
            $queryBuilder->quote('some-string'),
        );

        // Returns a sub-expression (casting "8" as integer) to return "g-string"
        $expression3 = $queryBuilder->expr()->right(
            $queryBuilder->expr()->castInt('(8)'),
            $queryBuilder->quote('some-very-log-string'),
        );

        // Return the right-side 23 characters from column "table_field_name"
        $expression4 = $queryBuilder->expr()->right(
            23,
            $queryBuilder->quoteIdentifier('table_field_name'),
        );
    }
}
Copied!

ExpressionBuilder::rightPad() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
rightPad ( string $value, string|int $length, string $paddingValue, string $asIdentifier = '')
param $value

Value, identifier or expression defining the value which should be right padded

param $length

Value, identifier or expression defining the padding length to fill up or crop

param $paddingValue

Padding character used to fill up if characters are missing on the right side

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns database connection platform compatible right-pad expression

Returns
string

Right-pad the value or sub-expression result with $paddingValue, to a total length of $length.

SQLite does not support RPAD("value", length, "paddingValue"), therefore a more complex compatible replacement expression construct is created.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateRightPad(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Right-pad the string "123" up to ten times with "0", resulting in "1230000000"
        $expression1 = $queryBuilder->expr()->rightPad(
            $queryBuilder->quote('123'),
            10,
            '0',
        );

        // Right-pad the cnotents of field "uid" up to ten times with 0, for uid=1 results in "1000000000".
        $expression2 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->castVarchar($queryBuilder->quoteIdentifier('uid')),
            10,
            '0',
        );

        // Right-pad the results of concatenating "1" + "2" + "3" ("123") up to 10 times with 0, resulting in "1230000000"
        $expression3 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->concat(
                $queryBuilder->quote('1'),
                $queryBuilder->quote('2'),
                $queryBuilder->quote('3'),
            ),
            10,
            '0',
        );

        // Left-pad the result of sub-expression casting "1123" to a string,
        // resulting in "1123000000""
        $expression4 = $queryBuilder->expr()->rightPad(
            $queryBuilder->expr()->castVarchar('( 1123 )'),
            10,
            '0',
        );

        // Right-pad the string "123" up to 10 times with "0" and make the result ("1230000000") available as "virtual_field"
        $expression5 = $queryBuilder->expr()->rightPad(
            $queryBuilder->quote('123'),
            10,
            '0',
            'virtual_field',
        );
    }
}
Copied!

ExpressionBuilder::space() 

New in version 13.1

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
space ( string|int $numberOfSpaces, string $asIdentifier = '')
param $numberOfSpaces

Statement or value defining how often a space should be repeated. Proper quoting must be ensured.

param $asIdentifier

Provide AS identifier if not empty, default: ''

Return description

Returns the platform compatible statement to create the x-times repeated space(s).

Returns
string

Create a statement containing $numberOfSpaces space characters.

The SPACE(numberOfSpaces) expression is used for MariaDB and MySQL and ExpressionBuilder::repeat() expression as a fallback for PostgreSQL and SQLite.

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\ConnectionPool;

final class MyTableRepository
{
    private const TABLE_NAME = 'pages';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function demonstrateSpace(): void
    {
        $queryBuilder = $this->connectionPool
            ->getQueryBuilderForTable(self::TABLE_NAME);
        // Returns "          " (10 space characters)
        $expression1 = $queryBuilder->expr()->space(
            '10',
        );

        // Returns "          " (10 space characters) and makes available as "aliased_field"
        $expression2 = $queryBuilder->expr()->space(
            '20',
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Return amount of space characters based on calculation (10 spaces)
        $expression3 = $queryBuilder->expr()->space(
            '(7+2+1)',
        );

        // Return amount of space characters based on a fixed value (210 spaces) and make available as "aliased_field"
        $expression3 = $queryBuilder->expr()->space(
            '(210)',
            $queryBuilder->quoteIdentifier('aliased_field'),
        );

        // Return a space X times, where X is the contents of the field table_repeat_number_field
        $expression5 = $queryBuilder->expr()->space(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('table_repeat_number_field'),
            ),
        );

        $expression6 = $queryBuilder->expr()->space(
            $queryBuilder->expr()->castInt(
                $queryBuilder->quoteIdentifier('table_repeat_number_field'),
            ),
            $queryBuilder->quoteIdentifier('aliased_field'),
        );
    }
}
Copied!

ExpressionBuilder::trim() 

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
trim ( string $fieldName, \Doctrine\DBAL\Platforms\TrimMode $position = \Doctrine\DBAL\Platforms\TrimMode::UNSPECIFIED, ?string $char = NULL)

Creates a TRIM expression for the given field.

param $fieldName

Field name to build expression for

param $position

Either constant out of LEADING, TRAILING, BOTH, default: DoctrineDBALPlatformsTrimMode::UNSPECIFIED

param $char

Character to be trimmed (defaults to space), default: NULL

Returns
string

Using the ->trim() expression ensures that fields are trimmed at the database level. The following examples give a better idea of what is possible:

EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;

final class MyTableRepository
{
    private const TABLE_NAME = 'tt_content';
    public function __construct(private readonly ConnectionPool $connectionPool) {}

    public function findFieldThatIsEmptyWhenTrimmed(string $fieldName): QueryBuilder
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
        $queryBuilder->expr()->comparison(
            $queryBuilder->expr()->trim($fieldName),
            ExpressionBuilder::EQ,
            $queryBuilder->createNamedParameter('', Connection::PARAM_STR),
        );
        return $queryBuilder;
    }
}
Copied!

Read how to correctly instantiate a query builder with the connection pool. See available parameter types.

The call to $queryBuilder->expr()-trim() can be one of the following:

  • trim('fieldName') results in TRIM("tableName"."fieldName")
  • trim('fieldName', TrimMode::LEADING, 'x') results in TRIM(LEADING "x" FROM "tableName"."fieldName")
  • trim('fieldName', TrimMode::TRAILING, 'x') results in TRIM(TRAILING "x" FROM "tableName"."fieldName")
  • trim('fieldName', TrimMode::BOTH, 'x') results in TRIM(BOTH "x" FROM "tableName"."fieldName")

Restriction builder 

Database tables in TYPO3 that can be managed in the backend have TCA definitions that specify how single fields and rows of the table should be handled and displayed by the framework.

The ctrl section of a table's TCA array specifies optional framework-internal handling of soft deletes and language overlays: For instance, when a row is deleted in the backend using the page or list module, many tables are configured to not drop that row entirely from the table, but to set a field (often deleted) for that row from 0 to 1. Similar mechanisms apply for start and end times, and to language and workspace overlays as well. See the Table properties (ctrl) chapter in the TCA reference for details on this topic.

However, these mechanisms come at a price: developers of extensions dealing with low-level queries must take care that overlaid or deleted rows are not included in the result set of a simple query.

This is where this "automatic restriction" enters the picture: The construct is created on top of native Doctrine DBAL as a TYPO3-specific extension. It automatically adds WHERE expressions that suppress rows which are marked as deleted or have exceeded their "active" lifecycle. All this is based on the TCA configuration of the affected table.

Rationale 

A developer might ask why they need to do all this to themselves, and why this extra material is added on top of a low-level query layer when "just a simple query" should be fired. The construct implements some important design goals:

  • Simple: Query creation should be easy to handle without a developer having to deal too much with the tedious TCA details..
  • Cope with developer laziness: If the framework would force a developer to always add casual restrictions for every single query, this is easy to forget. We are all lazy, aren't we?
  • Security: When in doubt, it is better to show a little too little than too much. It is much better to deal with a customer complaining that some records are not displayed than to show too many records. The former is "just a bug", while the latter can easily lead to a serious privilege escalation security issue.
  • Automatic query upgrades: If a table was originally designed without a soft delete and a delete flag is later added and registered in TCA, queries executed on that table will automatically upgrade and the according deleted = 0 restriction will be added.
  • Handing over restriction details to the framework: Having the restriction expressions handled by the framework gives it the ability to change details without breaking the extension code. This may well happen in the future, and a happy little upgrade path for such cases may prove very useful later.
  • Flexibility: The class construct is designed in a way so that developers can extend or or substitute it with their own restrictions if that makes sense for modeling the domain in question.

Main construct 

The restriction builder is called whenever a SELECT or COUNT query is executed using either \TYPO3\CMS\Core\Database\Query\QueryBuilder or \TYPO3\CMS\Core\Database\Connection . The QueryBuilder allows manipulation of those restrictions, while the simplified Connection class does not. When a query deals with multiple tables in a join, restrictions are added for all affected tables.

Each single restriction such as a \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction or a \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction is modeled as a single class that implements the \TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionInterface . Each restriction looks up in TCA whether it should be applied. If so, the according expressions are added to the WHERE clause when compiling the final statement.

Multiple restrictions can be grouped into containers which implement the \TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface .

The \TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer is always added by default: It adds the

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction ,
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction and the
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction .

Note that this applies to all contexts in which a query is executed: It does not matter whether a query is created from a frontend, a backend, or a CLI call, they all add the DefaultRestrictionContainer unless explicitly stated otherwise by an extension developer.

Restrictions 

\TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction (default)
Evaluates ['ctrl']['delete'], adds for instance AND deleted = 0 if TCA['aTable']['ctrl']['delete'] = 'deleted' is specified.
\TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction (default)
Evaluates ['ctrl']['enablecolumns']['disabled'], adds AND hidden = 0 if hidden is specified as field name.
\TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction (default)
Evaluates ['ctrl']['enablecolumns']['starttime'], typically adds something like AND (`tt_content`.`starttime` <= 1475580240).
\TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction (default)
Evaluates ['ctrl']['enablecolumns']['endtime'].
\TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction
Evaluates ['enablecolumns']['fe_group'].
\TYPO3\CMS\Core\Database\Query\Restriction\RootlevelRestriction
Match records on root level, adds AND (`pid` = 0)
\TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
The workspace restriction limits an SQL query to only select records which are "online" and in live or current workspace.

When a restriction needs to be enforced, a restriction could implement the interface \TYPO3\CMS\Core\Database\Query\Restriction\EnforceableQueryRestrictionInterface. If a restriction implements EnforceableQueryRestrictionInterface, the following applies:

  • ->removeAll() will remove all restrictions except the ones that implement the interface EnforceableQueryRestrictionInterface.
  • ->removeByType() will remove a restriction completely, also restrictions that implement the interface EnforceableQueryRestrictionInterface.

QueryRestrictionContainer 

\TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction

This container is always added if not told otherwise.

\TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer

Adds

  • \TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
  • \TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction

This container should be added by a developer to a query when creating query statements in frontend context or when handling frontend stuff from within CLI calls.

\TYPO3\CMS\Core\Database\Query\Restriction\LimitToTablesRestrictionContainer
This restriction container applies added restrictions only to the given table aliases. See Limit restrictions to tables for more information. Enforced restrictions are treated equally to all other restrictions.

Limit restrictions to tables 

With \TYPO3\CMS\Core\Database\Query\Restriction\LimitToTablesRestrictionContainer it is possible to apply restrictions to a query only for a given set of tables, or - to be precise - table aliases. Since it is a restriction container, it can be added to the restrictions of the query builder and can hold restrictions itself.

Examples 

If you want to apply one or more restrictions to only one table, that is possible as follows. Let us say you have content in the tt_content table with a relation to categories. Now you want to get all records with their categories except those that are hidden. In this case, the hidden restriction should apply only to the tt_content table, not to the sys_category or sys_category_*_mm table.

EXT:some_extension/Classes/Domain/Repository/ContentRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
    ->removeByType(HiddenRestriction::class)
    ->add(
        GeneralUtility::makeInstance(LimitToTablesRestrictionContainer::class)
            ->addForTables(GeneralUtility::makeInstance(HiddenRestriction::class), ['tt'])
    );
$queryBuilder->select('tt.uid', 'tt.header', 'sc.title')
    ->from('tt_content', 'tt')
    ->from('sys_category', 'sc')
    ->from('sys_category_record_mm', 'scmm')
    ->where(
        $queryBuilder->expr()->eq(
            'scmm.uid_foreign',
            $queryBuilder->quoteIdentifier('tt.uid')
        ),
        $queryBuilder->expr()->eq(
            'scmm.uid_local',
            $queryBuilder->quoteIdentifier('sc.uid')
        ),
        $queryBuilder->expr()->eq(
            'tt.uid',
            $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
        )
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool.

In addition, it is possible to restrict the complete set of restrictions of a query builder to a given set of table aliases:

EXT:some_extension/Classes/Domain/Repository/ContentRepository.php
// use TYPO3\CMS\Core\Database\Connection;

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
    ->removeAll()
    ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
$queryBuilder->getRestrictions()->limitRestrictionsToTables(['c2']);
$queryBuilder
    ->select('c1.*')
    ->from('tt_content', 'c1')
    ->leftJoin('c1', 'tt_content', 'c2', 'c1.parent_field = c2.uid')
    ->orWhere(
        $queryBuilder->expr()->isNull('c2.uid'),
        $queryBuilder->expr()->eq(
            'c2.pid',
            $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)
        )
    );
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Which results in:

SELECT "c1".*
  FROM "tt_content" "c1"
  LEFT JOIN "tt_content" "c2" ON c1.parent_field = c2.uid
  WHERE (("c2"."uid" IS NULL) OR ("c2"."pid" = 1))
    AND ("c2"."hidden" = 0))
Copied!

Custom restrictions 

It is possible to add additional query restrictions by adding class names as key to $GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'] . These restriction objects will be added to any select query executed using the QueryBuilder.

If these added restriction objects additionally implement \TYPO3\CMS\Core\Database\Query\Restriction\EnforceableQueryRestrictionInterface and return true in the to be implemented method isEnforced(), calling $queryBuilder->getRestrictions()->removeAll() such restrictions will still be applied to the query.

If an enforced restriction must be removed, it can still be removed with $queryBuilder->getRestrictions()->removeByType(SomeClass::class).

Implementers of custom restrictions can therefore have their restrictions always enforced, or even not applied at all, by returning an empty expression in certain cases.

To add a custom restriction class, use the following snippet:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class] ??= [];
Copied!

Removing third party restrictions is possible, by setting the option disabled for a restriction to true in global TYPO3 configuration or ext_localconf.php of an extension:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class]['disabled'] = true;
Copied!

Examples 

Often the default restrictions are sufficient. Nothing needs to be done in those cases.

However, many backend modules still want to show disabled records and remove the start time and end time restrictions to allow administration of those records for an editor. A typical setup from within a backend module:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// SELECT `uid`, `bodytext` FROM `tt_content` WHERE (`pid` = 42) AND (`tt_content`.`deleted` = 0)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
// Remove all restrictions but add DeletedRestriction again
$queryBuilder
    ->getRestrictions()
    ->removeAll()
    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where($queryBuilder->expr()->eq(
        'pid',
        $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
    ))
    ->executeQuery()
    ->fetchAllAssociative(();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

The DeletedRestriction should be kept in almost all cases. Usually, the only extension that dismisses that flag is the recycler module to list and resurrect deleted records. Any object implementing the QueryRestrictionInterface can be given to ->add(). This allows extensions to deliver own restrictions.

An alternative to the recommended way of first removing all restrictions and then adding needed ones again (using ->removeAll(), then ->add()) is to kick specific restrictions with a call to ->removeByType():

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
// use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
// Remove starttime and endtime, but keep hidden and deleted
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
    ->getRestrictions()
    ->removeByType(StartTimeRestriction::class)
    ->removeByType(EndTimeRestriction::class);
Copied!

Read how to correctly instantiate a query builder with the connection pool.

In the frontend it is often needed to swap the DefaultRestrictionContainer with the FrontendRestrictionContainer:

EXT:some_extension/Classes/SomeClass.php
// use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
// Remove default restrictions and add list of default frontend restrictions
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
Copied!

Note that ->setRestrictions() resets any previously specified restrictions. Any class instance implementing QueryRestrictionContainerInterface can be given to ->setRestrictions(). This allows extensions to deliver and use an custom set of restrictions for own query statements if needed.

Result 

A \Doctrine\DBAL\Result object is returned by QueryBuilder->executeQuery() for ->select() and ->count() query types, and by Connection->select() and Connection->count() calls.

The object represents a query result set and has methods to fetch single rows with ->fetchAssociative() or to fetch all rows as an array with ->fetchAllAssociative().

fetchAssociative() 

This method fetched the next row from the result. It is usually used in while() loops. This is the recommended way of accessing the result in most use cases.

Typical example:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$result = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
  ->executeQuery();

while ($row = $result->fetchAssociative()) {
    // Do something useful with that single $row
}
Copied!

Read how to correctly instantiate a query builder with the connection pool.

->fetchAssociative() returns an array reflecting one result row with field/value pairs in one call and retrieves the next row with the next call. It returns false when no more rows can be found.

fetchAllAssociative() 

This method returns an array containing all rows of the result set by internally implementing the same while loop as above. Using that method saves some precious code characters, but is more memory intensive if the result set is large and contains many rows and data, since large arrays are carried around in PHP:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Fetch all records from tt_content on page 42
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'bodytext')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

fetchOne() 

The method returns a single column from the next row of a result set, other columns from this result row are discarded. It is especially handy for QueryBuilder->count() queries:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
// Get the number of tt_content records on pid 42 into variable $numberOfRecords
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$numberOfRecords = $queryBuilder
    ->count('uid')
    ->from('tt_content')
    ->where(
        $queryBuilder->expr()->eq(
            'pid',
            $queryBuilder->createNamedParameter(42, Connection::PARAM_INT)
        )
    )
    ->executeQuery()
    ->fetchOne();
Copied!

Read how to correctly instantiate a query builder with the connection pool.

rowCount() 

This method returns the number of rows affected by the last execution of this statement. Use this method instead of counting the number of records in a ->fetchAssociative() loop manually.

Reuse prepared statement 

Doctrine DBAL usually prepares a statement first and then executes it with the given parameters. The implementation of prepared statements depends on the particular database driver. A driver that does not implement prepared statements properly falls back to a direct execution of a given query.

There is an API that allows to make real use of prepared statements. This is handy when the same query is executed over and over again with different arguments. The example below prepares a statement for the pages table and executes it twice with different arguments.

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
// use TYPO3\CMS\Core\Database\Connection;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$statement = $queryBuilder
    ->select('uid')
    ->from('pages')
    ->where(
        $queryBuilder->expr()->eq(
            'uid',
            $queryBuilder->createPositionalParameter(0, Connection::PARAM_INT)
        )
    )
    ->prepare();

$pages = [];
foreach ([24, 25] as $pageId) {
    // Bind $pageId value to the first (and in this case only) positional parameter
 $statement->bindValue(1, $pageId, Connection::PARAM_INT);
 $result = $statement->executeQuery();
    $pages[] = $result->fetchAssociative();
    $result->free(); // free the resources for this result
}
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Looking at a MySQL debug log:

Prepare SELECT `uid` FROM `pages` WHERE `uid` = ?
Execute SELECT `uid` FROM `pages` WHERE `uid` = '24'
Execute SELECT `uid` FROM `pages` WHERE `uid` = '25'
Copied!

The log shows one statement preparation with two executions.

Doctrine DBAL driver middlewares 

New in version 12.3

Introduction 

Doctrine DBAL supports custom driver middlewares since version 3. These middlewares act as a decorator around the actual Driver component. Subsequently, the Connection, Statement and Result components can be decorated as well. These middlewares must implement the \Doctrine\DBAL\Driver\Middleware interface. A common use case would be a middleware to implement SQL logging capabilities.

For more information on driver middlewares, see the Architecture chapter of the Doctrine DBAL documentation. Furthermore, look up the implementation of the EXT:adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php (GitHub) in the Admin Panel system extension as an example.

Global driver middlewares and driver middlewares for a specific connection are combined for a connection. They are sortable.

Register a global driver middleware 

New in version 13.0

Global driver middlewares are applied to all configured connections.

Example:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-global-driver-middleware'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Disable a global middleware for a specific connection 

Example:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

// Register a global middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-global-driver-middleware'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];

// Disable a global driver middleware for a specific connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['my-ext/custom-global-driver-middleware']['disabled'] = true;
Copied!

Register a driver middleware for a specific connection 

New in version 12.3

Deprecated since version 13.0

In this example, the custom driver middleware MyDriverMiddleware is added to the Default connection:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier'] = [
    'target' => MyDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Migration 

For example:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier']
    = MyDriverMiddlewareClass::class;
Copied!

needs to be converted to:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier'] = [
    'target' => MyDriverMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Registration for driver middlewares for TYPO3 v12 and v13 

Extension authors providing dual Core support in one extension version can use the Typo3Version class to provide the configuration suitable for the Core version and avoiding the deprecation notice:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\MyDriverMiddleware;
use TYPO3\CMS\Core\Information\Typo3Version;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['driver-middleware-identifier']
    = ((new Typo3Version())->getMajorVersion() < 13)
    ? MyDriverMiddleware::class
    : [
        'target' => MyDriverMiddleware::class,
        'after' => [
            // NOTE: Custom driver middleware should be registered after essential
            //       TYPO3 Core driver middlewares. Use the following identifiers
            //       to ensure that.
            'typo3/core/custom-platform-driver-middleware',
            'typo3/core/custom-pdo-driver-result-middleware',
        ],
    ];
Copied!

Sorting of driver middlewares 

New in version 13.0

Global driver middlewares and connection driver middlewares are combined for a connection.

TYPO3 makes the global and connection driver middlewares sortable similar to the PSR-15 middleware stack. The available structure for a middleware configuration is:

target

target
Data type

string

Required

yes

The fully-qualified class name of the driver middleware.

before

before
Data type

list of strings

Required

no

Default

[]

A list of middleware identifiers the current middleware should be registered before.

after

after
Data type

list of strings

Required

no

Default

[]

A list of middleware identifiers the current middleware should be registered after.

disabled

disabled
Data type

boolean

Required

no

Default

false

It can be used to disable a global middleware for a specific connection.

Example:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Doctrine\Driver\CustomGlobalDriverMiddleware;

defined('TYPO3') or die();

// Register global driver middleware
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['global-driver-middleware-identifier'] = [
    'target' => CustomGlobalDriverMiddleware::class,
    'disabled' => false,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
    'before' => [
        'some-driver-middleware-identifier',
    ],
];

// Disable a global driver middleware for a connection
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['global-driver-middleware-identifier'] = [
    // To disable a global driver middleware, setting disabled to true for a
    // connection is enough. Repeating target, after and/or before configuration
    // is not required.
    'disabled' => true,
];
Copied!

The interface UsableForConnectionInterface 

New in version 13.0

Doctrine DBAL driver middlewares can be registered globally for all connections or for specific connections. Due to the nature of the decorator pattern, it may become hard to determine for a specific configuration or drivers, if a middleware needs to be executed only for a subset, for example, only specific drivers.

TYPO3 provides a custom \TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface driver middleware interface which requires the implementation of the method canBeUsedForConnection():

interface UsableForConnectionInterface
Fully qualified name
\TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface

Custom driver middleware can implement this interface to decide per connection and connection configuration if it should be used or not. For example, registering a global driver middleware which only takes affect on connections using a specific driver like pdo_sqlite.

Usually this should be a rare case and mostly a driver middleware can be simply configured as a connection middleware directly, which leaves this more or less a special implementation detail for the TYPO3 core.

canBeUsedForConnection ( string $identifier, array $connectionParams)

Return true if the driver middleware should be used for the concrete connection.

param $identifier

the identifier

param $connectionParams

the connectionParams

Returns
bool

This allows to decide, if a middleware should be used for a specific connection, either based on the $connectionName or the $connectionParams, for example the concrete $connectionParams['driver'].

Example 

The custom driver:

EXT:my_extension/Classes/DoctrineDBAL/CustomDriver.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DoctrineDBAL;

use Doctrine\DBAL\Driver\Connection as DriverConnection;
// Using the abstract class minimize the methods to implement and therefore
// reduces a lot of boilerplate code. Override only methods that needed to be
// customized.
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;

final class CustomDriver extends AbstractDriverMiddleware
{
    public function connect(#[\SensitiveParameter] array $params): DriverConnection
    {
        $connection = parent::connect($params);

        // Do something custom on connect, for example wrapping the driver
        // connection class or executing some queries on connect.

        return $connection;
    }
}
Copied!

The custom driver middleware which implements the \TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface :

EXT:my_extension/Classes/DoctrineDBAL/CustomMiddleware.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DoctrineDBAL;

use Doctrine\DBAL\Driver as DoctrineDriverInterface;
use Doctrine\DBAL\Driver\Middleware as DoctrineDriverMiddlewareInterface;
use MyVendor\MyExtension\DoctrineDBAL\CustomDriver as MyCustomDriver;
use TYPO3\CMS\Core\Database\Middleware\UsableForConnectionInterface;

final class CustomMiddleware implements DoctrineDriverMiddlewareInterface, UsableForConnectionInterface
{
    public function wrap(DoctrineDriverInterface $driver): DoctrineDriverInterface
    {
        // Wrap the original or already wrapped driver with our custom driver
        // decoration class to provide additional features.
        return new MyCustomDriver($driver);
    }

    public function canBeUsedForConnection(
        string $identifier,
        array $connectionParams,
    ): bool {
        // Only use this driver middleware, if the configured connection driver
        // is 'pdo_sqlite' (sqlite using php-ext PDO).
        return ($connectionParams['driver'] ?? '') === 'pdo_sqlite';
    }
}
Copied!

Register the custom driver middleware:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\DoctrineDBAL\CustomMiddleware;

defined('TYPO3') or die();

// Register middleware globally, to include it for all connections which
// uses the 'pdo_sqlite' driver.
$GLOBALS['TYPO3_CONF_VARS']['DB']['globalDriverMiddlewares']['my-ext/custom-pdosqlite-driver-middleware'] = [
    'target' => CustomMiddleware::class,
    'after' => [
        // NOTE: Custom driver middleware should be registered after essential
        //       TYPO3 Core driver middlewares. Use the following identifiers
        //       to ensure that.
        'typo3/core/custom-platform-driver-middleware',
        'typo3/core/custom-pdo-driver-result-middleware',
    ],
];
Copied!

Various tips and tricks 

  • Use Find usages of PhpStorm for examples! The source code of the Core is a great way to learn how specific methods of the API are used. In PhpStorm it is extremely helpful to right-click on a single method and list all method usages with Find usages. This is especially handy to quickly see usage examples for complex methods like join() from the query builder.
  • INSERT, UPDATE and DELETE statements are often better to read and write using the Connection object instead of the query builder.
  • SELECT DISTINCT aField is not supported but can be substituted with a ->groupBy('aField').
  • getSql() and executeQuery() / executeStatement() can be used after each other during development to simplify debugging:

    EXT:my_extension/Classes/Domain/Repository/MyRepository.php
    $queryBuilder
        ->select('uid')
        ->from('tt_content')
        ->where(
            $queryBuilder->expr()->eq(
                'bodytext',
                $queryBuilder->createNamedParameter('lorem')
            )
        );
    
    debug($queryBuilder->getSql());
    
    $result = $queryBuilder->executeQuery();
    Copied!
  • Doctrine DBAL throws exceptions if something goes wrong when calling API methods. The exception type is \Doctrine\DBAL\Exception. Typical extensions should usually not catch such exceptions but let it bubble up to be handled by the global TYPO3 core error and exception handling: They most often indicate a broken connection, database schema or programming error and extensions should usually not try to hide away or escalate them on their own.
  • count() <database-query-builder-count> query types using the query builder normally call ->fetchOne() to receive the count value. The count() method of the Connection object does this automatically and returns the result of the count value directly.

Troubleshooting 

About database error "Row size too large" 

MySQL and MariaDB can generate a "Row size too large" error when modifying tables with numerous columns. TYPO3 version 13 has implemented measures to mitigate this issue in most scenarios. We refer to the changelog: Important: #104153 - About database error "Row size too large".

Database compare during update and installation 

Whenever you install or update an extension, or change the TCA definition or the ext_tables.sql in an extension, you have to take into account the fact that the database schema might have changed.

TYPO3 backend with the Maintenance Admin Tools. The database analyzer is highlighted.

Here system maintainers can compare the database schema and apply any changes.

Compare the database schema and apply changes 

Users with System Maintainer privileges can use the Analyze Database Structure section in the Admin Tools > Maintenance module to compare the defined schema with the current one. The module display options to incorporate changes by adding, removing, or updating columns.

You can also use the console command typo3 extension:setup to add tables and columns defined by installed or updated extensions:

vendor/bin/typo3 extension:setup
Copied!
typo3/sysext/core/bin/typo3 extension:setup
Copied!

Adding columns and tables is safe 

Adding additional columns or tables is not problematic. You can safely add any column shown as missing.

Deleting columns or tables: be careful 

Columns suggested for deletion might still be needed by upgrade wizards.

Before deleting tables or columns with the database analyzer:

  • Run all upgrade wizards
  • Make a database backup

Some third-party extensions may rely on database columns or tables they do not explicitly define. Removing them could cause these extensions to break.

Changing a column type: it depends 

Some column changes extend capabilities and are safe. For example:

  • Changing from TEXT to LONGTEXT allows more data to be stored and does not affect existing content.

Other changes can cause problems if existing data violates the new definition. For instance:

  • Changing from NULL to NOT NULL will fail if any row, including soft-deleted ones (deleted = 1), still contains NULL.

Some extensions provide upgrade wizards to clean or convert data. Note that many wizards ignore soft-deleted records. Deleting unnecessary soft-deleted records may help.

Conflicting column definitions 

Database structure is defined by the Table Configuration Array (TCA) and by definitions in the ext_tables.sql file in an extension, if the file exists.

If two extensions define the same column in different ways, the definition from the extension that is loaded last will take precedence.

This means that an extension that changes or adds columns to a table must declare a dependency on the original extension to ensure proper loading order.

Extbase persistence – models and the database 

Extbase provides its own way to persist and retrieve data using models and repositories, which are built on top of TYPO3's database abstraction layer.

Repositories in Extbase usually define custom find*() methods and rely on \TYPO3\CMS\Extbase\Persistence\Generic\Query to perform queries on models.

While Extbase persistence is the standard way to work with data in Extbase, you can also use the DBAL QueryBuilder directly within an Extbase context when:

  • You need better performance on large datasets.
  • You are performing complex queries (aggregates like SUM, AVG, ...).

Reference index 

The reference index in TYPO3 is the table sys_refindex. (Related link: Soft references). The table contains all relations/cross correlations between datasets. For example a content element has an image and a link. These two references can be found in this table stored against this unique data record (tt_content uid).

When you want to perform a TYPO3 update it is recommended to update these relations. See Update Reference Index.

To perform an update you can use the TYPO3 Console command shown in that section.

TYPO3 installations with a small number of records can use the module System > DB check and use the Manage Reference Index function.

On TYPO3 installations with a large number of records and many relations between those the maximum run time of PHP will be reached and the scripts therefore fail. It is recommended to run the commands from the command line. This module outputs the commands with absolute paths to update or check the reference from the command line.

Tables can be excluded from the reference index by using the event IsTableExcludedFromReferenceIndexEvent.

Introduction 

Database 

The DataHandler is the class that handles all data writing to database tables configured in TCA. In addition the class handles commands such as copy, move, delete. It will handle undo/history and versioning of records and everything will be logged to the sys_log table. It will make sure that write permissions are evaluated correctly for the user trying to write to the database. Generally, any processing specific option in the $GLOBALS['TCA'] array is handled by DataHandler.

Using DataHandler for manipulation of the database content in the TCA-configured tables guarantees that the data integrity of TYPO3 is respected. This cannot be safely guaranteed, if you write to TCA-configured database tables directly. It will also manage the relations to files and other records.

DataHandler requires a backend login to work. This is due to the fact that permissions are observed (of course) and thus DataHandler needs a backend user to evaluate against. This means you cannot use DataHandler from the frontend scope. Thus writing to tables (such as a guestbook) will have to be done from the frontend without DataHandler.

Files 

DataHandler can also handle files. The file operations are normally performed in the File > Filelist module where you can manage a directory on the server by copying, moving, deleting and editing files and directories. The file operations are managed by two Core classes, \TYPO3\CMS\Core\Utility\File\BasicFileUtility and \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility .

DataHandler basics 

Introduction 

When you are using DataHandler from your backend applications you need to prepare two arrays of information which contain the instructions to DataHandler ( \TYPO3\CMS\Core\DataHandling\DataHandler ) of what actions to perform. They fall into two categories: data and commands.

"Data" is when you want to write information to a database table or create a new record.

"Commands" is when you want to move, copy or delete a record in the system.

The data and commands are created as multidimensional arrays, and to understand the API of DataHandler you need to understand the hierarchy of these two arrays.

Basic usage 

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function basicUsage(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $cmd = [];
        $data = [];
        $dataHandler->start($data, $cmd);

        // ... do something more ...
    }
}
Copied!

After this initialization you usually want to perform the actual operations by calling one (or both) of these two methods:

$this->dataHandler->process_datamap();
$this->dataHandler->process_cmdmap();
Copied!

Commands array 

Syntax:

$cmd[ tablename ][ uid ][ command ] = value
Copied!

Description of keywords in syntax:

tablename

tablename
Data type
string

Name of the database table. It must be configured in the $GLOBALS['TCA'] array, otherwise it cannot be processed.

uid

uid
Data type
integer

The UID of the record that is manipulated. This is always an integer.

command

command
Data type
string (command keyword)

The command type you want to execute.

See command keywords and values

value

value
Data type
mixed

The value for the command.

See command keywords and values

Command keywords and values 

copy

copy
Data type
integer or array

The significance of the value depends on whether it is positive or negative:

Positive value
The value points to a page UID. A copy of the record (and possibly child elements/tree below) will be inserted inside that page as the first element.
Negative value
The (absolute) value points to another record from the same table as the record being copied. The new record will be inserted on the same page as that record and if $GLOBALS['TCA'][$table]['ctrl']['sortby'] is set, then it will be positioned after.
Zero value
Record is inserted on tree root level.
array

The array has to contain the integer value as in examples above and may contain field => value pairs for updates. The array is structured like:

[
    'action' => 'paste', // 'paste' is used for both move and copy commands
    'target' => $pUid,   // Defines the page to insert the record, or record uid to copy after
    'update' => $update, // Array with field => value to be updated.
]
Copied!

move

move
DataType
integer

Works like copy but moves the record instead of making a copy.

delete

delete
Data Type
integer (1)

Value should always be "1".

This action will delete the record (or mark the record "deleted", if configured in $GLOBALS['TCA'][$table]['ctrl']['delete']).

undelete

undelete
Data Type
integer (1)

Value should always be "1".

This action will set the "deleted" flag back to 0.

localize

localize
Data type
integer

The value is the languageId (defined in the site configuration) to localize the record into. Basically a localization of a record is making a copy of the record (possibly excluding certain fields defined with l10n_mode) but changing relevant fields to point to the right language ID.

Requirements for a successful localization is this:

  • [ctrl] options languageField and transOrigPointerField must be defined for the table
  • A languageId must be configured in the site configuration.
  • The record to be localized by currently be set to default language and not have any value set for the TCA transOrigPointerField either.
  • There cannot exist another localization to the given language for the record (looking in the original record PID).

Apart from this, ordinary permissions apply as if the user wants to make a copy of the record on the same page.

The localize DataHandler command should be used when translating records in "connected mode" (strict translation of records from the default language). This command is used when selecting the "Translate" strategy in the content elements translation wizard.

copyToLanguage

copyToLanguage
Data type
integer

It behaves like localize command (both record and child records are copied to given language), but does not set transOrigPointerField fields (for example, l10n_parent).

The copyToLanguage command should be used when localizing records in the "free mode". This command is used when localizing content elements using translation wizard's "Copy" strategy.

inlineLocalizeSynchronize

inlineLocalizeSynchronize
Data type
array

Performs localization or synchronization of child records. The command structure is like:

$cmd['tt_content'][13]['inlineLocalizeSynchronize'] = [ // 13 is a parent record uid
    'field' => 'tx_myfieldname', // field we want to synchronize
    'language' => 2,             // uid of the target language
    // either the key 'action' or 'ids' must be set
    'action' => 'localize',      // or 'synchronize'
    'ids' =>  [1, 2, 3],         // array of child IDs to be localized
];
Copied!

version

version
Data type
array

Versioning action.

Keys:

[action]

Keyword determining the versioning action. Options are:

"new"

Indicates that a new version of the record should be created. Additional keys, specific for "new" action:

[treeLevels]

(Only pages) Integer, -1 to 4, indicating the number of levels of the page tree to version together with a page. This is also referred to as the versioning type:

  • -1 ("element") means only the page record gets versioned (default)
  • 0 ("page") means the page + content tables (defined by ctrl flag versioning_followPages )
  • >0 ("branch") means the the whole branch is versioned (full copy of all tables), down to the level indicated by the value (1 = 1 level down, 2 = 2 levels down, etc.). The treeLevel is recorded in the field t3ver_swapmode and will be observed when the record is swapped during publishing.
[label]
Indicates the version label to apply. If not given, a standard label including version number and date is added.
"swap"

Indicates that the current online version should be swapped with another. Additional keys, specific for "swap" action:

[swapWith]
Indicates the uid of the record to swap current version with!
[swapIntoWS]
Boolean, indicates that when a version is published it should be swapped into the workspace of the offline record.
"clearWSID"
Indicates that the workspace of the record should be set to zero (0). This removes versions out of workspaces without publishing them.
"flush"
Completely deletes a version without publishing it.
"setStage"

Sets the stage of an element. Special feature: The id key in the array can be a comma-separated list of ids in order to perform the stageChange over a number of records. Also, the internal variable ->generalComment (also available through `/record/commit` route as `&generalComment`) can be used to set a default comment for all stage changes of an instance of the data handler. Additional keys for this action are:

[stageId]

Values are:

  • -1 (rejected)
  • 0 (editing, default)
  • 1 (review),
  • 10 (publish)
[comment]
Comment string that goes into the log.

Examples of commands 

EXT:my_extension/Classes/DataHandling/MyClass.php
$cmd['tt_content'][54]['delete'] = 1;    // Deletes tt_content record with uid=54
$cmd['tt_content'][1203]['copy'] = -303; // Copies tt_content uid=1203 to the position after tt_content uid=303 (new record will have the same pid as tt_content uid=1203)
$cmd['tt_content'][1203]['copy'] = 400;  // Copies tt_content uid=1203 to first position in page uid=400
$cmd['tt_content'][1203]['move'] = 400;  // Moves tt_content uid=1203 to the first position in page uid=400
Copied!

Accessing the uid of copied records 

The DataHandler keeps track of records created by copy operations in its $copyMappingArray_merged property. This property is public but marked as @internal. So it is subject to change in future TYPO3 versions without notice.

The $copyMappingArray_merged property can be used to determine the UID of a record copy based on the UID of the copied record.

The structure of the $copyMappingArray_merged property looks like this:

EXT:my_extension/Classes/DataHandling/MyClass.php
$copyMappingArray_merged = [
   <table> => [
      <original-record-uid> => <record-copy-uid>,
   ],
];
Copied!

The property contains the names of the manipulated tables as keys and a map of original record UIDs and UIDs of record copies as values.

EXT:my_extension/Classes/DataHandling/MyClass.php
$cmd['tt_content'][1203]['copy'] = 400;  // Copies tt_content uid=1203 to first position in page uid=400
$this->dataHandler->start([], $cmd);
$this->dataHandler->process_cmdmap();

$uid = $this->dataHandler->copyMappingArray_merged['tt_content'][1203];
Copied!

Data array 

Syntax: $data['<tablename>'][<uid>]['<fieldname>'] = 'value'

Description of keywords in syntax:

tablename

tablename
Data type
string

Name of the database table. There must be a configuration for the table in $GLOBALS['TCA'] array, otherwise it cannot be processed.

uid

uid
Data type
string|int

The UID of the record that is modified. If the record already exists, this is an integer.

If you are creating new records, use a random string prefixed with NEW, for example, NEW7342abc5e6d. You can use static strings (NEW1, NEW2, ...) or generate them using \TYPO3\CMS\Core\Utility\StringUtility::getUniqueId('NEW').

fieldname

fieldname
Data type
string

Name of the database field you want to set a value for. The columns of the table must be configured in $GLOBALS['TCA'][$table]['columns'].

value

value
Data type
string

Value for "fieldname".

For fields of type inline this is a comma-separated list of UIDs of referenced records.

Examples of data submission 

This creates a new page titled "The page title" as the first page inside page id 45:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'The page title',
    'subtitle' => 'Other title stuff',
    'pid' => '45'
];
Copied!

This creates a new page titled "The page title" right after page id 45 in the tree:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'The page title',
    'subtitle' => 'Other title stuff',
    'pid' => '-45'
];
Copied!

This creates two new pages right after each other, located right after the page id 45:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages']['NEW9823be87'] = [
    'title' => 'Page 1',
    'pid' => '-45'
];
$data['pages']['NEWbe68s587'] = [
    'title' => 'Page 2',
    'pid' => '-NEW9823be87'
];
Copied!

Notice how the second "pid" value points to the "NEW..." id placeholder of the first record. This works because the new id of the first record can be accessed by the second record. However it works only when the order in the array is as above since the processing happens in that order!

This creates a new content record with references to existing and one new system category:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['sys_category']['NEW9823be87'] = [
    'title' => 'New category',
    'pid' => 1,
];
$data['tt_content']['NEWbe68s587'] = [
    'header' => 'Look ma, categories!',
    'pid' => 45,
    'categories' => [
        1,
        2,
        'NEW9823be87', // You can also use placeholders here
    ],
];
Copied!

This updates the page with uid=9834 to a new title, "New title for this page", and no_cache checked:

EXT:my_extension/Classes/DataHandling/MyClass.php
$data['pages'][9834] = [
    'title' => 'New title for this page',
    'no_cache' => '1'
];
Copied!

Clear cache 

DataHandler also has an API for clearing the cache tables of TYPO3:

EXT:my_extension/Classes/DataHandling/MyClass.php
$this->dataHandler->clear_cacheCmd($cacheCmd);
Copied!

Values for the $cacheCmd argument:

[integer]

[integer]

Clear the cache for the page ID given.

"all"

"all"

Clears all cache tables (cache_pages, cache_pagesection, cache_hash).

Only available for admin-users unless explicitly allowed by User TSconfig "options.clearCache.all".

"pages"

"pages"

Clears all pages from cache_pages.

Only available for admin-users unless explicitly allowed by User TSconfig "options.clearCache.pages".

Clear cache using cache tags 

Every processing of data or commands is finalized with flushing a few caches in the pages group. Cache tags are used to specifically flush the relevant cache entries instead of the cache as whole.

By default the following cache tags are flushed:

  • The table name of the updated record, for example, pages when updating a page or tx_myextension_mytable when updating a record of this table.
  • A combination of table name and record UID, for example, pages_10 when updating the page with UID 10 or tx_myextension_mytable_20 when updating the record with UID 20 of this table.
  • A page UID prefixed with pageID_ ( pageId_<page-uid>), for example, pageId_10 when updating a page with UID 10 (additionally all related pages, see clearcache-pagegrandparent and clearcache-pagesiblingchildren) and pageId_10 when updating a record if a record of any table placed on the page with UID 10 ( <table>.pid = 10) is updated.

Notice that you can also use the \TYPO3\CMS\Core\Cache\CacheDataCollector::addCacheTags method to register additional tags for the cache entry of the current page while it is rendered. This way you can implement an elaborate caching behavior which ensures that every record update in the TYPO3 backend (which is processed by the DataHandler) automatically flushes the cache of all pages where that record is displayed.

Following the rules mentioned above you could register cache tags from within your Extbase plugin (for example, controller or a custom ViewHelper):

EXT:my_extension/Classes/Controller/SomeController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\ExampleModel;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Cache\CacheDataCollector;
use TYPO3\CMS\Core\Cache\CacheTag;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class SomeController extends ActionController
{
    public function showAction(ExampleModel $example): ResponseInterface
    {
        // ...

        /** @var CacheDataCollector $cacheDataCollector */
        $cacheDataCollector = $this->request->getAttribute('frontend.cache.collector');
        $cacheDataCollector->addCacheTags(
            new CacheTag(sprintf('tx_myextension_example_%d', $example->getUid())),
        );

        // ...
    }
}
Copied!

New in version 13.3

The frontend.cache.collector request attribut has been introduced as a successor of the now deprecated TypoScriptFrontendController->addCacheTags() method. Switch to another version of this page for an example in an older TYPO3 version. For compatibility with TYPO3 v12 and v13 use TypoScriptFrontendController->addCacheTags().

Hook for cache post-processing 

You can configure cache post-processing with a user defined PHP function. Configuration of the hook can be done from ext_localconf.php. An example might look like:

EXT:my_extension/ext_localconf.php
<?php

declare(strict_types=1);

use Vendor\SomeExtension\Hook\DataHandlerHook;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] =
    DataHandlerHook::class . '->postProcessClearCache';
Copied!

Flags in DataHandler 

There are a few internal variables you can set prior to executing commands or data submission. These are the most significant:

->copyTree

->copyTree
Data type
integer
Default
0

Sets the number of branches on a page tree to copy.

0
Branch is not copied
1
Pages on the first level are copied.
2
Pages on the second level are copied.

And so on.

->reverseOrder

->reverseOrder
Data type
boolean
Default
false

If set, the data array is reversed in the order, which is a nice thing if you are creating a whole bunch of new records.

->copyWhichTables

->copyWhichTables
Data type
list of strings (tables)
Default
"*"

This list of tables decides which tables will be copied. If empty then none will. If "*" then all will (that the user has permission to, of course).

Using the DataHandler in scripts 

You can use the class \TYPO3\CMS\Core\DataHandling\DataHandler in your own scripts: Inject the DataHandler class, build a $data/ $cmd array you want to pass to the class, and call a few methods.

Using the DataHandler in a Symfony command 

It is possible to use the DataHandler for scripts started from the command line or by the scheduler as well. You can do this by creating a Symfony Command.

These scripts use the _cli_ backend user. Before using the DataHandler in your execute() method, you should make sure that this user is initialized like this:

EXT:my_extension/Classes/Command/MyCommand.php
\TYPO3\CMS\Core\Core\Bootstrap::initializeBackendAuthentication();
Copied!

If you forget to add the backend user authentication, an error similar to this will occur:

[1.2.1]: Attempt to modify table "pages" without permission
Copied!

DataHandler examples 

What follows are a few code listings with comments which will provide you with enough knowledge to get started. It is assumed that you have populated the $data and $cmd arrays correctly prior to these chunks of code. The syntax for these two arrays is explained in the DataHandler basics chapter.

Submitting data 

This is the most basic example of how to submit data into the database.

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function submitData(): void
    {
        // Prepare the data array
        $data = [
            // ... the data ...
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // Register the $data array inside DataHandler and initialize the
        // class internally.
        $dataHandler->start($data, []);

        // Submit data and have all records created/updated.
        $dataHandler->process_datamap();
    }
}
Copied!

Executing commands 

The most basic way of executing commands:

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function executeCommands(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // Prepare the cmd array
        $cmd = [
            // ... the cmd structure ...
        ];

        // Registers the $cmd array inside the class and initialize the
        // class internally.
        $dataHandler->start([], $cmd);

        // Execute the commands.
        $dataHandler->process_cmdmap();
    }
}
Copied!

Clearing cache 

In this example the cache clearing API is used. No data is submitted, no commands are executed. Still you will have to initialize the class by calling the start() method (which will initialize internal state).

EXT:my_extension/Classes/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function clearCache(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start([], []);
        $dataHandler->clear_cacheCmd('all');
    }
}
Copied!

Caches are organized in groups. Clearing "all" caches will actually clear caches from the "all" group and not really all caches. Check the caching framework architecture section for more details about available caches and groups.

Complex data submission 

Imagine the $data array contains something like this:

$data = [
    'pages' => [
        'NEW_1' => [
            'pid' => 456,
            'title' => 'Title for page 1',
        ],
        'NEW_2' => [
            'pid' => 456,
            'title' => 'Title for page 2',
        ],
    ],
];
Copied!

This aims to create two new pages in the page with uid "456". In the following code this is submitted to the database. Notice the reversing of the order of the array: This is done because otherwise "page 1" is created first, then "page 2" in the same PID meaning that "page 2" will end up above "page 1" in the order. Reversing the array will create "page 2" first and then "page 1" so the "expected order" is preserved.

To insert a record after a given record, set the other record's negative uid as pid in the new record you're setting as data.

Apart from this a "signal" will be send that the page tree should be updated at the earliest occasion possible. Finally, the cache for all pages is cleared.

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function submitComplexData(): void
    {
        $data = [
            'pages' => [
                'NEW_1' => [
                    'pid' => 456,
                    'title' => 'Title for page 1',
                ],
                'NEW_2' => [
                    'pid' => 456,
                    'title' => 'Title for page 2',
                ],
            ],
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $dataHandler->reverseOrder = true;
        $dataHandler->start($data, []);
        $dataHandler->process_datamap();
        BackendUtility::setUpdateSignal('updatePageTree');
        $dataHandler->clear_cacheCmd('pages');
    }
}
Copied!

Both data and commands executed with alternative user object 

In this case it is shown how you can use the same object instance to submit both data and execute commands if you like. The order will depend on the order in the code.

First the start() method is called, but this time with the third possible argument which is an alternative $GLOBALS['BE_USER'] object. This allows you to force another backend user account to create stuff in the database. This may be useful in certain special cases. Normally you should not set this argument since you want DataHandler to use the global $GLOBALS['BE_USER'].

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function useAlternativeUser(BackendUserAuthentication $alternativeBackendUser): void
    {
        // Prepare the data array
        $data = [
            // ... the data ...
        ];

        // Prepare the cmd array
        $cmd = [
            // ... the cmd structure ...
        ];

        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        $dataHandler->start($data, $cmd, $alternativeBackendUser);
        $dataHandler->process_datamap();
        $dataHandler->process_cmdmap();
    }
}
Copied!

Error handling 

The data handler has a property errorLog as an array. In this property, the data handler collects all errors. You can use these, for example, for logging or other error handling.

EXT:my_extension/Classes/DataHandling/MyClass.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\DataHandling;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final class MyClass
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function handleError(): void
    {
        /** @var DataHandler $dataHandler */
        // Do not inject or reuse the DataHander as it holds state!
        // Do not use `new` as GeneralUtility::makeInstance handles dependencies
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);

        // ... previous call of DataHandler's process_datamap() or process_cmdmap()

        if ($dataHandler->errorLog !== []) {
            $this->logger->error('Error(s) while creating content element');
            foreach ($dataHandler->errorLog as $log) {
                // handle error, for example, in a log
                $this->logger->error($log);
            }
        }
    }
}
Copied!

The "/record/commit" route 

This route is a gateway for posting form data to the \TYPO3\CMS\Backend\Controller\SimpleDataHandlerController .

You can send data to this file either as GET or POST vars where POST takes precedence. The variable names you can use are:

data

data
Data type
array

Data array on the form [tablename][uid][fieldname] = value.

Typically it comes from a POST form which submits a form field like <input name="data[tt_content][123][header]" value="This is the headline">.

cmd

cmd
Data type
array

Command array on the form [tablename][uid][command] = value. This array may get additional data set internally based on clipboard commands send in CB var!

Typically this comes from GET vars passed to the script like &cmd[tt_content][123][delete]=1 which will delete the content element with UID 123.

cacheCmd

cacheCmd
Data type
string

Cache command sent to DataHandler->clear_cacheCmd().

redirect

redirect
Data type
string

Redirect URL. The script will redirect to this location after performing operations (unless errors has occurred).

flags

flags
Data type
array

Accepts options to be set in DataHandler object. Currently, it supports "reverseOrder" (boolean).

mirror

mirror
Data type
array

Example: [mirror][table][11] = '22,33' will look for content in [data][table][11] and copy it to [data][table][22] and [data][table][33].

CB

CB
Data type
array

Clipboard command array. May trigger changes in "cmd".

vC

vC
Data type
string

Verification code.

Debugging 

PHP 

TYPO3 backend debug mode 

To display additional debug information in the backend, set $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] in config/system/settings.php and log in with an administrator account.

It shows for example the names of fields and in case of select, radio and checkbox fields the values in addition, which are generated by the FormEngine. These can be used to set access permissions or configuration using Page TSconfig.

If EXT:lowlevel is installed, the name of the database table or field is appended to the select options in the System > DB Check > Full Search module.

Additionally, in debug mode, the page renderer does not compress or concatenate JavaScript or CSS resources.

DebugUtility::debug() 

The TYPO3 Core provides a simple debug() (defined in EXT:core/Classes/Core/GlobalDebugFunctions.php). It wraps around \TYPO3\CMS\Core\Utility\DebugUtility::debug() and will output debug information only if it matches a set of IP addresses (defined in $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']).

For example, the following code:

Extension examples, file Classes/Controller/ModuleController.php
use TYPO3\CMS\Core\Utility\DebugUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    protected function debugCookies() {
        DebugUtility::debug($_COOKIE, 'cookie');
    }
}
Copied!

will produce such an output:

Typical TYPO3 debug output

In general, look at class \TYPO3\CMS\Core\Utility\DebugUtility for useful debugging tools.

Extbase DebuggerUtility 

Extbase's DebuggerUtility::var_dump() is a debugging function in TYPO3 that outputs detailed, human-readable information about variables, including their type and structure. It offers features like depth control and optional backtrace information to assist developers in effectively debugging complex data structures.

Example:

\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($myVariable);

You can also use the Extbase DebuggerUtility to debug SQL Querys for example. To do so, put the following code snippet before the execute function of your SQL query:

\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($queryBuilder->getSQL());

Fluid Debug ViewHelper 

The Fluid Debug ViewHelper is a part of the Fluid template engine and generates a HTML dump of the tagged variable. The ViewHelper can be used in any Fluid template to output the value of variables or objects in a human-readable format.

Example:

<f:debug>{myVariable}</f:debug>

To display all available variables in your Fluid template, you can use the _all placeholder:

<f:debug>{_all}</f:debug>

Get more information in the Fluid ViewHelper Reference Debug ViewHelper <f:debug>

Xdebug 

First of all: best practice to debug PHP code is using an IDE (like PhpStorm or Visual Studio Code) with Xdebug. Advantages are:

  • You can investigate the values of variables and class properties when setting a breakpoint.
  • You can move forward line by line and see how the variables and properties change.
  • You can also step into calling methods and functions.
  • You see the calling code as a stack trace.

Dependency injection 

Abstract 

This chapter explains "Dependency Injection (DI)" as used in TYPO3. Readers interested in the general concepts and principles may want to look at, for example, Dependency Injection in "PHP The Right Way" and What is dependency injection? by Fabien Potencier. Whenever a service has a service dependency to another class, the technique of dependency injection should be used to satisfy that need. TYPO3 uses a Symfony component for dependency injection. The component is PSR-11 compliant, and is used throughout core and extensions to standardize the process of obtaining service dependencies.

By default all API services shipped with the TYPO3 Core system extensions offer dependency injection. The recommended usage is constructor injection. Available as well are method injection and interface injection. To activate the Symfony component for dependency injection a few lines of configuration are necessary.

Introduction 

The title of this chapter is "dependency injection" (DI), but the scope is a bit broader: In general, this chapter is about TYPO3 object lifecycle management and how to obtain services, with one sub-part of it being dependency injection.

The underlying interfaces are based on the PHP-FIG (PHP Framework Interop Group) standard PSR-11 Container interface, and the implementation is based on Symfony service container and Symfony dependency injection components, plus some TYPO3 specific sugar.

Background and history 

Obtaining object instances in TYPO3 has always been straightforward: Call GeneralUtility::makeInstance(\MyVendor\MyExtension\Some\Class::class) and hand over mandatory and optional __construct() arguments as additional arguments.

There are two quirks to that:

  • First, a class instantiated through GeneralUtility::makeInstance() can implement SingletonInterface. This empty interface definition tells makeInstance() to instantiate the object exactly once for this request, and if another makeInstance() call asks for it, the same object instance is returned - otherwise makeInstance() always creates a new object instance and returns it. Implementing SingletonInterface is nowadays considered old-fashioned, its usage should be reduced over time.
  • Second, GeneralUtility::makeInstance() allows "XCLASSing". This is a - rather dirty - way to substitute a class with a custom implementation. XCLASS'ing in general is brittle and seen as a last resort hack if no better solution is available. In connection with Symfony containers, XCLASSing services should be avoided in general and service overrides should be used instead. Replacing the XCLASS functionality in TYPO3 is still work in progress. In contrast, XCLASSing is still useful for data objects, and there is no good other solution yet.

Using makeInstance() worked very well for a long time. It however lacked a feature that has been added to the PHP world after makeInstance() had been invented: Dependency injection. There are lots of articles about dependency injection on the net, so we won't go too deep here but rather explain the main idea: The general issue appears when classes follow the separation of concerns principle.

One of the standard examples is logging. Let's say a class's responsibility is the creation of users - it checks everything and finally writes a row to database. Now, since this is an important operation, the class wants to log an information like "I just created a user". And this is where dependency injection enters the game: Logging is a huge topic, there are various error levels, information can be written to various destinations and so on. The little class does not want to deal with all those details, it just wants to tell the framework: "Please give me some logger I can use and that takes care of all details, I don't want to know about them". This separation is the heart of single responsibility and separation of concerns.

Dependency injection does two things for us here: First, it allows separating concerns, and second, it hands the task of finding an appropriate implementation of a dependency over to the framework, so the framework decides - based on configuration - which specific instance is given to the consumer. Note in our example, the logging instance itself may have dependencies again - the process of object creation and preparation may be nested.

In more abstract software engineering terms: Dependency injection is a pattern used to delegate the task of resolving class dependencies away from a consumer towards the underlying framework.

Back to history: After makeInstance() has been around for quite a while and lacked an implementation of dependency injection, Extbase appeared in 2009. Extbase brought a first container and dependency injection solution, it's main interface being the Extbase ObjectManager. The Extbase object manager has been widely used for a long time, but suffered from some issues younger approaches don't face. One of the main drawbacks of Extbase object manager is the fact that it's based on runtime reflection: Whenever an object is to be instantiated, the object manager scans the class for needed injections and prepares dependencies to be injected. This process is quite slow though mitigated by various caches. And these also come with costs. In the end, these issues have been the main reason the object manager was never established as a main core concept but only lived in Extbase scope.

The object lifecycle and dependency injection solution based on Symfony DI has been added in TYPO3v10 and is a general core concept: Next to the native dependency injection, it is also wired into makeInstance() as a long living backwards compatibility solution, and it fully substitutes the Extbase object manager. In contrast to the Extbase solution, Symfony based object management does not have the overhead of expensive runtime calculations. Instead it is an instance wide build-time solution: When TYPO3 bootstraps, all object creation details of all classes are read from a single cache file just once, and afterwards no expensive calculation is required for actual creation.

Symfony based DI was implemented in TYPO3 v10 and usage of the Extbase ObjectManager was discouraged.

The Extbase ObjectManager has been removed with TYPO3 v12. Making use of Symfony DI integration continues. There are still various places in the core to be improved. Further streamlining is done over time. For instance, the final fate of makeInstance() and the SingletonInterface has not fully been decided on. Various tasks remain to be solved in younger TYPO3 development to further improve the object lifecycle management provided by the core.

Build-time caches 

To get a basic understanding of the core's lifecycle management it is helpful to get a rough insight on the main construct. As already mentioned, object lifecycle management is conceptualized as steps to take place at build-time. It is done very early and only once during the TYPO3 bootstrap process. All calculated information is written to a special cache that can not be reconfigured and is available early. On subsequent requests the cache file is loaded. Extensions can not mess with the construct if they adhere to the core API.

Besides being created early, the state of the container is independent and exactly the same in frontend, backend and CLI scope. The same container instance may even be used for multiple requests. This is becoming more and more important nowadays with the core being able to execute sub requests. The only exception to this is the Install Tool: It uses a more basic container that "cannot fail". This difference is not important for extension developers however since they can't hook into the Install Tool at those places.

The Symfony container implementation is usually configured to actively scan the extension classes for needed injections. All it takes are just a couple of lines within the Services.yaml file. This should be done within all extensions that contain PHP classes and it is the fundamental setup we will outline in the following sections.

For developers, it is important to understand that dealing with Symfony DI is an early core bootstrap and cached thing. The system will fail upon misconfiguration, leading to unreachable frontend and backend.

With the container cache entry being a low level early bootstrap thing that is expensive to calculate when it has to be rebuild, there is a limited list of options to flush this cache:

  • The container cache entry is not deleted when a backend user clicks "Flush all caches" in the backend top toolbar if the instance is configured as production application. For developer convenience, the container cache is flushed in development context, though.
  • The container cache is flushed using "Admin tools" -> "Maintenance" -> "Flush Caches" of the Install Tool.
  • The container cache is flushed using the CLI command vendor/bin/typo3 cache:flush. Using vendor/bin/typo3 cache:warmup afterwards will rebuild and cache the container.
  • The container cache is automatically flushed when using the Extension Manager to load or unload extensions in (non-Composer) classic mode.
  • Another way to quickly drop this cache during development is to remove all var/cache/code/di/* files, which reside in typo3temp/ in classic mode instances or elsewhere in composer mode instances (see Environment).

The main takeaway is: When a developer fiddles with container configuration, the cache needs to be manually cleared. And if some configuration issue slipped in, which made the container or DI calculation fail, the system does not heal itself and needs both a fix of the Dependency injection configuration plus probably a cache removal. The standalone Install Tool however should always work, even if the backend breaks down, so the "Flush caches" button is always reachable. Note that if the container calculation fails, the var/log/typo3_* files contain the exception with backtrace!

Important terms 

We will continue to use a couple of technical terms in this chapter, so let's quickly define them to align. Some of them are not precisely used in our world, for instance some Java devs may stumble upon "our" understanding of a prototype.

Prototype
The broad understanding of a prototype within the TYPO3 community is that it's simply an object that is created anew every time. Basically the direct opposite of a singleton. In fact, the prototype pattern describes a base object that is created once, so __construct() is called to set it up, after that it is cloned each time one wants to have a new instance of it. The community isn't well aware of that, and the core provides no "correct" prototype API, so the word prototype is often misused for an object that is always created anew when the framework is asked to create one.
Singleton
A singleton is an object that is instantiated exactly once within one request, with the same instance being re-used when a class instance is requested. Symfony understands such class instances as being "shared". TYPO3 can also declare a class as "shared" using the SingletonInterface , but this is considered old-fashioned. Services are usually declared shared by default. This implies such classes should be stateless and there is trouble ahead when they are not.
Service
We use the understanding "What is a service?" from Symfony: In Symfony, everything that is instantiated through the service container is a service. These are many things - for instance controllers are services, as well as - non static - utilities, repositories and classes like mailers and similar. To emphasize: Not only classes named with a *Service suffix are services but basically anything as long as it is not a data object. A class should represent either-or: A class is either a service that manipulates or does something with given data and does not hold it, or is a class that holds data. Sadly, this distinction is not always the case within TYPO3 core (yet), and there are many classes that blend service functionality and data characteristics.
Data object
Data objects are the opposite of services. They are not available through service containers. Calling $container->has() returns false and they can not be injected. They should be instantiated using new(). Domain models and DTOs are a typical example of data objects. Data objects are not "service container aware" and do not support DI. Although the TYPO3 core does not strictly follow this rule in all cases until now, the ambition is to get this done over time.

Using DI 

The general idea is: Whenever your service class has a service dependency to another class, dependency injection should be used.

In some TYPO3 APIs dependency injection cannot be used yet. This applies to classes that need specific data in their constructors or classes that are serialized and deserialized as, for example, scheduler tasks. The TYPO3 core tries to refactor these cases over time. Such classes need to fall back to old-school GeneralUtility::makeInstance()

There are two ways proclaimed and natively supported by TYPO3 to obtain service dependencies: Constructor injection using __construct() and method injection using inject*() methods. Constructor injection is the way to go as long as a class is not dealing with complex abstract inheritance chains. The symfony service container can inject specific classes as well as instances of interfaces.

Constructor injection 

Lets say we're writing a controller that renders a list of users. Those users are found using a UserRepository service, making the user repository service a direct dependency of the controller service. A typical constructor dependency injection to resolve the dependency by the framework looks like this:

EXT:my_extension/Controller/UserController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}
}
Copied!

The symfony container setup process will now see UserRepository as a dependency of UserController when scanning its __construct() method. Since autowiring is enabled by default (more on that below), an instance of the UserRepository is created and provided to __construct() when the controller is created. The instance is set as a class property using constructor property promotion and the property is declared readonly.

Method injection 

A second way to get services injected is by using inject*() methods:

EXT:my_extension/Controller/UserController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Repository\UserRepository;

final class UserController
{
    private ?UserRepository $userRepository = null;

    public function injectUserRepository(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
}
Copied!

This ends up with basically the same result as above: The controller instance retrieves an object of type UserRepository in class property $userRepository. The service container calls such inject*() methods directly after class instantiation, so after __construct() has been executed, and before anything else. The injection via methods was introduced by Extbase. TYPO3 core implemented it in addition to the default Symfony constructor injection. Why did we do that, you may ask? Both strategies have subtle differences: First, when using inject*() methods, the type hinted class property needs to be nullable, otherwise PHP >= 7.4 throws a warning since the instance is not set during __construct(). But that's just an implementation detail. More important is an abstraction scenario. Consider this case:

EXT:my_extension/Controller/UserController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Logger\Logger;
use MyVendor\MyExtension\Repository\UserRepository;

abstract class AbstractController
{
    protected ?Logger $logger = null;

    public function injectLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
}

final class UserController extends AbstractController
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}
}
Copied!

We have an abstract controller service with a dependency plus a controller service that extends the abstract and has further dependencies.

Now assume the abstract class is provided by TYPO3 core and the consuming class is provided by an extension. If the abstract class would use constructor injection, the consuming class would need to know the dependencies of the abstract, add its own dependencies to the constructor, and then call parent::__construct($logger) to satisfy the abstracts dependency. This would hardcode all dependencies of the abstract into extending classes. If later the abstract is changed and another dependency is added to the constructor, this would break consuming classes.

Differently put: When core classes "pollute" __construct() with dependencies, the core can not add dependencies without being breaking. This is the reason why for example the extbase AbstractController uses inject*() methods for its dependencies: Extending classes can then use constructor injection, do not need to call parent::__construct(), and the core is free to change dependencies of the abstract.

In general, when the core provides abstract classes that are expected to be extended by extensions, the abstract class should use inject*() methods instead of constructor injection. Extensions of course can follow this idea in similar scenarios.

This construct has some further implications: Abstract classes should think about making their dependency properties private, so extending classes can not rely on them. Furthermore, classes that should not be extended by extensions are free to use constructor injection and should be marked final, making sure they can't be extended to allow internal changes.

Interface injection 

EXT:my_extension/Controller/UserController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Logger\LoggerInterface;

final class UserController extends AbstractController
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}
Copied!

Notice the difference? The code requests the injection of an interface and not a class! This is permissible with both constructor and method injection. It compels the service container to determine which specific class is configured as the implementation of the interface and inject an instance of that class. A class can declare itself as the default implementation of such an interface. This is the essence of dependency injection: a consuming class no longer relies on a specific implementation but on an interface's signature.

The framework ensures that something fulfilling the interface is injected. The consuming class remains unaware of the specific implementation, focusing solely on the interface methods. An instance administrator can configure the framework to inject a different implementation, either globally or for specific classes. The consumer remains unconcerned, interacting only with the interface methods.

The example below has a couple of controller classes as service consumers. There is a service interface with a default implementation. The default implementation uses the symfony PHP attribute AsAlias to register itself as default. A Services.yaml file configures different service implementation for some service consumers:

EXT:my_extension/Controller/MyFirstController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MyFirstController
{
    public function __construct(
        private readonly MyServiceInterface $myService,
    ) {}
}
Copied!
EXT:my_extension/Controller/MySecondController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MySecondController
{
    public function __construct(
        private readonly MyServiceInterface $myService,
    ) {}
}
Copied!
EXT:my_extension/Controller/MyThirdController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MyThirdController
{
    private MyServiceInterface $myService;

    public function injectMyService(MyServiceInterface $myService): void
    {
        $this->myService = $myService;
    }
}
Copied!
EXT:my_extension/Service/MyServiceInterface.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

interface MyServiceInterface
{
    public function foo();
}
Copied!
EXT:my_extension/Service/MyDefaultServiceImplementation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(MyServiceInterface::class)]
class MyDefaultServiceImplementation implements MyServiceInterface
{
    public function foo()
    {
        // do something
    }
}
Copied!
EXT:my_extension/Service/MyOtherServiceImplementation.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

class MyOtherServiceImplementation implements MyServiceInterface
{
    public function foo()
    {
        // do something
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  # Within MySecondController and MyThirdController different implementations
  # than the default MyDefaultServiceImplementation of MyServiceInterface
  # shall be injected.

  # When working with constructor injection
  MyVendor\MyExtension\Controller\MySecondController:
    arguments:
      $service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'

  # When working with method injection
  MyVendor\MyExtension\Controller\MyThirdController:
    calls:
      - method: 'injectMyService'
        arguments:
          $service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'
Copied!

Configuration 

Services.yaml declaring service defaults 

Extensions have to configure their classes to make use of dependency injection. This can be done in Configuration/Services.yaml. Alternatively, Configuration/Services.php can also be used, if PHP syntax is required to apply conditional logic to definitions. A basic Services.yaml file of an extension looks like the following.

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

  MyVendor\MyExtension\:
    resource: '../Classes/*'
    exclude: '../Classes/Domain/Model/*'
Copied!
autowire

autowire: true instructs the dependency injection component to calculate the required dependencies from type declarations. The calculation generates service initialization code.

An extension is not required to use autowiring. It can manually wire dependencies. However, opting out of autowiring is less convenient and is not further documented in this guide.

autoconfigure

This directive instructs the dependency injection component to automatically add Symfony service tags based on implemented interfaces and base classes. For instance, autoconfiguration ensures that classes implementing SingletonInterface are publicly available from the Symfony container and marked as shared ( shared: true).

TYPO3 dependency injection relies on this this for various default configurations. It is recommended to set autoconfigure: true.

public
public: false is a performance optimization and should therefore be set in extensions. This settings controls which services are available through the dependency injection container used internally by GeneralUtility::makeInstance(). See "What to make public?" for more information.
Model exclusion
The path exclusion exclude: '../Classes/Domain/Model/*' excludes your models from the dependency injection container: You cannot inject them nor inject dependencies into them. Models are not services but data objects and therefore should not require dependency injection. Also, these objects are usually created by the Extbase persistence layer, which does not support the DI container.

Autoconfiguration using attributes and Services.yaml 

Single service classes may need to change auto configuration to be different than above declared defaults. This can be done using PHP attributes. The most common use case is public: true:

EXT:my_extension/Services/MyServiceUsingAutoconfigurePublicTrue.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

/**
 * This service is instantiated using GeneralUtility::makeInstance()
 * in some cases, which requires 'public' being set to 'true'.
 */
#[Autoconfigure(public: true)]
readonly class MyServiceUsingAutoconfigurePublicTrue
{
    public function __construct(
        private SomeDependency $someDependency,
    ) {}
}
Copied!

The above usage of the Autoconfigure attribute declares this service as public: true which overrides a public: false default from a Services.yaml file for this specific class.

Similar with shared: false:

EXT:my_extension/Services/MyServiceUsingAutoconfigureSharedFalse.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

/**
 * This service is stateful and configures the service container to
 * inject new instances to consuming services when they are instantiated.
 */
#[Autoconfigure(shared: false)]
class MyServiceUsingAutoconfigureSharedFalse
{
    private string $foo = 'foo';

    public function __construct(
        private readonly SomeDependency $someDependency,
    ) {}

    public function setFoo(): void
    {
        $this->foo = 'bar';
    }
}
Copied!

It is possible to set both using #[Autoconfigure(public: true, shared: false)].

The Autoconfigure attribute is beneficial when an extension includes a service class that is either stateful or instantiated using GeneralUtility::makeInstance(). This attribute embeds the configuration directly within the class file, eliminating the need for additional entries in Services.yaml - the configuration is "in place".

To reconfigure "foreign" services - those not provided by the extension itself but by another extension (such as a service class from ext:core) - the Services.yaml file can be utilized. A common scenario is when a core service is not declared public because all core extensions retrieve instances via constructor or method injection, rather than GeneralUtility::makeInstance(). If an extension must use GeneralUtility::makeInstance() for a specific reason, it can declare the "foreign" service as "public" in Services.yaml:

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

  MyVendor\MyExtension\:
    resource: '../Classes/*'

  # Declare a "foreign" service "public: true" since this extension needs
  # to instantiate the service using GeneralUtility::makeInstance() and
  # the service is configured "public: false" by the extension delivering
  # that service.
  TYPO3\CMS\Core\Some\Service:
    public: true
Copied!

Autowiring using attributes 

Autowiring, particularly the Autowire PHP attribute, is a powerful tool for making dependency injection more convenient and transparent. TYPO3 core includes default configurations that facilitate its use. Let’s explore some examples.

Consider a service performing an expensive operation that caches the result within the TYPO3 runtime cache to avoid repeating the operation within the same request. The runtime cache, being a dependent service, should be injected. A naive approach is to inject the core CacheManager and retrieve the runtime cache instance:

EXT:my_extension/Services/MyServiceUsingCacheManager.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

class MyServiceUsingCacheManager
{
    private FrontendInterface $runtimeCache;

    public function __construct(
        CacheManager $cacheManager,
    ) {
        $this->runtimeCache = $cacheManager->getCache('runtime');
    }

    public function calculateSomethingExpensive()
    {
        // do something using runtime cache
    }
}
Copied!

This can be simplified, resulting in more streamlined code:

EXT:my_extension/Services/MyServiceGettingRuntimeCacheInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

class MyServiceGettingRuntimeCacheInjected
{
    public function __construct(
        #[Autowire(service: 'cache.runtime')]
        private readonly FrontendInterface $runtimeCache,
    ) {}

    public function calculateSomethingExpensive()
    {
        // do something using runtime cache
    }
}
Copied!

The "cache.runtime" service alias, configured by the TYPO3 core extension, performs the CacheManager->getCache() operation behind the scenes. Utilizing such shortcuts simplifies the consumers.

The autowire attribute also enables the execution of expressions and injection of the results, which is useful for "compile-time" state that remains constant during requests. For example, to inject a feature toggle status:

EXT:my_extension/Services/MyServiceGettingFeatureToggleResultInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyServiceGettingFeatureToggleResultInjected
{
    public function __construct(
        #[Autowire(expression: 'service("features").isFeatureEnabled("myExtension.foo")')]
        private readonly bool $fooEnabled,
    ) {}
}
Copied!

Another example, including alias definition, is new in TYPO3 v13. It enables injecting values from ext_conf_templates.txt files using the ExtensionConfiguration API.

EXT:core/Configuration/ExtensionConfiguration.php
<?php

declare(strict_types=1);

namespace TYPO3\CMS\Core\Configuration;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('extension-configuration', public: true)]
class ExtensionConfiguration
{
    public function get(string $extension, string $path = ''): mixed
    {
        // implementation
    }
}
Copied!
EXT:my_extension/Services/MyServiceGettingExtensionConfigurationValueInjected.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MyServiceGettingExtensionConfigurationValueInjected
{
    public function __construct(
        #[Autowire(expression: 'service("extension-configuration").get("my_extension", "something.isEnabled")')]
        private readonly bool $somethingIsEnabled,
    ) {}
}
Copied!

This example demonstrates the combination of a service class with an alias and a consumer utilizing this alias in an Autowire attribute.

The TYPO3 core provides a couple such service aliases, with the above ones being the most important ones for extension developers. TYPO3 core does not arbitrarily add aliases.

Installation-wide configuration 

A global service configuration for a project can be set up to be utilized across multiple project-specific extensions. This allows, for example, the aliasing of an interface with a concrete implementation that can be used in several extensions. Additionally, project-specific CLI commands can be registered without the need for a project-specific extension.

However, this is only possible - due to security restrictions - if TYPO3 is configured such that the project root is outside the document root, which is typically the case in Composer-based installations.

In Composer-based installations, the global service configuration files services.yaml and services.php are located within the config/system/ directory of a TYPO3 project.

Consider the following scenario: You want to use the PSR-20 clock interface as a type hint for dependency injection in the service classes of your project's various extensions. This setup allows the concrete implementation to change without altering your code. In this example, we use lcobucci/clock as the concrete implementation.

The global files services.yaml and services.php are read before files from extensions. The global files can provide defaults but can not override service definitions from service configuration files loaded afterwards.

config/system/services.yaml
services:

  Psr\Clock\ClockInterface:
    factory: ['Lcobucci\Clock\SystemClock', 'fromUTC']
Copied!

The concrete clock implementation is now injected when a type hint to the interface is given:

EXT:my_extension/Classes/MyServiceUsingClockInterface.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Clock\ClockInterface;

final class MyServiceUsingClockInterface
{
    public function __construct(
        private readonly ClockInterface $clock,
    ) {}

    // ...
}
Copied!

FAQ 

What to make public? 

The basic difference between public and private is well explained in the symfony documentation:

When defining a service, it can be made to be public or private. If a service is public, it means that you can access it directly from the container at runtime. For example, the doctrine service is a public service:

// only public services can be accessed in this way
$doctrine = $container->get('doctrine');
Copied!

But typically, services are accessed using dependency injection. And in this case, those services do not need to be public.

So unless you specifically need to access a service directly from the container via $container->get(), the best-practice is to make your services private.

The implementation of GeneralUtility::makeInstance() utilizes $container->get(). As a result, services instantiated using makeInstance() must be declared public if they have dependencies that need to be injected.

Services without dependencies can be instantiated using makeInstance() without the service made public, as they are instantiated using new without constructor arguments.

Some services are automatically declared public by basic TYPO3 dependency injection configuration since they are instantiated using makeInstance() by the core framework. The most common ones are:

  • Extbase controllers implementing ControllerInterface , usually by inheriting ActionController . They are additionally declared shared: false.
  • Backend controllers with AsController class attribute. They are additionally declared shared: false:

    use TYPO3\CMS\Backend\Attribute\AsController;
    
    #[AsController]
    final readonly class MyBackendController
    {
        // implementation
    }
    Copied!
  • Classes implementing SingletonInterface
  • Fluid view helpers implementing \ViewHelperInterface . They are additionally declared shared: false.
  • Fluid data processors tagged with data.processor.

Examples of classes that must be made public:

Services that use dependency injection and are not declared public typically error out with typical messages when instantiated using makeInstance() They should be declared public:

Unsatisfied constructor injection
(1/1) ArgumentCountError

Too few arguments to function MyVendor\MyExtension\Namespace\Class::__construct(),
0 passed in typo3/sysext/core/Classes/Utility/GeneralUtility.php on line 3461 and exactly 1 expected
Copied!
Unsatisfied method injection
(1/1) Error

Call to a member function methodName() on null
Copied!

How to override service arguments? 

Some services in the TYPO3 core use service arguments, which can be overridden by third-party extensions. For example, the $rateLimiterFactory argument in the \ControllerPasswordRecoveryController of the typo3/cms-felogin extension uses a service with the ID feloginPasswordRecovery.rateLimiterFactory. This service is defined in the Services.yaml file of ext:felogin and includes a service argument named $config, which specifies the configuration for the Symfony Rate Limiter used in the class.

A third-party extension can override the feloginPasswordRecovery.rateLimiterFactory service in its own Services.yaml file, as shown in the example below:

packages/my-extension/Configuration/Services.yaml
feloginPasswordRecovery.rateLimiterFactory:
  class: Symfony\Component\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'felogin-password-recovery'
      policy: 'sliding_window'
      limit: 10
      interval: '30 minutes'
    $storage: '@storage.cachingFramework'
Copied!

It is important that the 3rd party extension is loaded after the extension whose service it overrides. This can be achieved by requiring the original extension as a dependency in the packages/my-extension/composer.json file of the 3rd party extension.

What do declare shared: false? 

A service declared shared: false is not a singleton. Instead, a new instance is created each time the consuming service is instantiated. This approach makes the consuming service stateful as well, but declaring shared: false can help mitigate side effects between different services. It is often preferable to create stateful services using GeneralUtility::makeInstance() when needed, rather than within __construct().

When to use GeneralUtility::makeInstance()? 

Ideally, all services in a framework are stateless: They depend on other stateless services and are always retrieved using dependency injection.

TYPO3 core development is gradually transitioning more services to be stateless. However, many historically stateful services still exist. The critical point is that injecting a stateful service into a stateless service makes the consumer stateful as well. This can create a chain of coupled stateful services, leading to unexpected results when these services are reused multiple times within a single request. While declaring a service shared: false can mitigate the issue, it doesn't solve the underlying problem. This scenario is a primary use case for GeneralUtility::makeInstance(). Instead of injecting a stateful service at service build time and reusing it frequently, the service can use makeInstance() at runtime when it needs a service instance.

For instance, the DataHandler class should create new instances for each use, as it becomes "tainted" after use and cannot reset its state properly. Such "dirty-after-use" services should be instantiated anew with makeInstance() when needed.

Some services are stateful but provide workarounds to be injectable. A good example is the Extbase UriBuilder . It is stateful due to its use of the method chaining pattern but includes a reset() method to reset its state. When used correctly, this service can be injected and reused. Additionally, UriBuilder is declared shared: false, so consumers receive distinct instances, reducing the risk of bugs from improper use of reset().

Various solutions exist to make existing services stateless. For instance, the extbase UriBuilder could deprecate its setX() chaining methods and introduce a UriParameterBag data object, which would be passed to the service worker methods. Implementing such changes in the TYPO3 core codebase is an ongoing process that requires careful consideration.

Deciding whether to use makeInstance() instead of dependency injection requires examining the dependency's behavior. Consider these factors:

  • The service class is declared readonly and only declares stateless dependencies in __construct().
  • The service has no class properties.
  • All __construct() arguments are services and declared readonly.
  • __construct() requires no manual non-service arguments.

The last point is particularly relevant: Some TYPO3 core services expect state to be passed to __construct(), making them stateful and unsuitable for injection, as dependency injection cannot handle consumer state. These services must be instantiated using makeInstance() until their constructors are updated to be compatible with dependency injection.

When to use new? 

Services should be always retrieved using dependency injection. If that is not feasible because the dependent service is stateful or because the class is created using a "polluted" constructor with manual arguments, it should be created using makeInstance(). While services without dependencies could be instantiated with new, this approach has drawbacks: It introduces risks if the service is later modified to include dependencies and bypasses the XCLASS mechanism and potential service overrides by configuration.

Only data objects - preferably using public constructor property promotion - should be instantiated using the PHP keyword new.

Mix manual constructor arguments and service dependencies? 

A service can not mix manual constructor arguments with service dependencies handled by dependency injection. Manual constructor arguments make services stateful. When a service is instantiated with manual arguments, such as $myService = GeneralUtility::makeInstance(MyService::class, $myState), dependency injection is bypassed, and any other service dependencies in the constructor are ignored. Mixing both blends the roles of services and data objects, which is poor PHP architecture.

The extbase-based dependency injection solution using ObjectManager allowed such mixtures, but this has been replaced by the Symfony-based dependency injection solution, which does not support this practice.

What about user functions? 

It is possible to use dependency injection when calling custom user functions, for example .userFunc within TypoScript or in (legacy) hooks, usually via \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction().

callUserFunction() internally uses the dependency-injection-aware helper GeneralUtility::makeInstance(), which can recognize and inject services that are marked public.

What about injection in a XCLASS'ed class? 

When extending an existing class (for example, an Extbase controller) using XCLASS and injecting additional dependencies using constructor injection, define the existing class as an alias for the extended class in the Configuration/Services.yaml file of the extending extension, by using the shortcut notation for an alias as shown in the example below:

EXT:my_extension/Configuration/Services.yaml
TYPO3\CMS\Belog\Controller\BackendLogController: '@MyVendor\MyExtension\Controller\ExtendedBackendLogController'
Copied!

If the extended class is instantiated by GeneralUtility::makeInstance() and must be declared public, either use an additional PHP attribute or the full alias notation including the public argument:

EXT:my_extension/Configuration/Services.yaml
TYPO3\CMS\Belog\Controller\BackendLogController:
  public: true
  alias: MyVendor\MyExtension\Controller\ExtendedBackendLogController
Copied!

Not yet exemplified 

  • This document does not currently elaborate on Symfony service providers, although the TYPO3 core uses them in various places. Use cases for these should be outlined.
  • The concept and usage of "lazy" services are not discussed.
  • Solutions to cyclic dependencies should be explored. Cyclic dependencies occur when services depend on each other, forming a graph instead of a tree, which Symfony's dependency injection cannot resolve. One solution is to make one side lazy, although this is not the primary use of "lazy." Another approach involves using a factory with an interface, as demonstrated in ext:styleguide.

Further information 

Deprecation handling: logging, marking and finding deprecations 

TYPO3 logs calls to deprecated functions to help developers identify and update outdated code. Deprecated methods will be removed in future TYPO3 versions, so they should be avoided.

Deprecations are triggered by trigger_error() and pass through TYPO3’s logging and exception system. In development, they are shown as exceptions by default; in production, they are typically ignored.

Enabling the deprecation log 

TYPO3 ships with a default configuration where deprecation logging is disabled. If you upgrade to the latest TYPO3 version, you need to change your development configuration to enable deprecation logging if you need it.

Via GUI 

Enabling the deprecation log can be done in the Admin Tools > Settings backend module. Click on Choose Preset in the Configuration Presets pane, open Debug settings, activate the Debug option and submit with Activate preset. Disabling the deprecation log can be done by selecting the Live preset instead.

Enabling the debug preset

Enabling the debug preset

The debug preset also enables some other debug settings.

Via configuration file directly 

Instead of using the GUI you can also enable or disable the deprecation log with the disabled option:

Excerpt of config/system/settings.php | typo3conf/system/settings.php
<?php

return [
    // ... some configuration
    'LOG' => [
        'TYPO3' => [
            'CMS' => [
                'deprecations' => [
                    'writerConfiguration' => [
                        'notice' => [
                            'TYPO3\CMS\Core\Log\Writer\FileWriter' => [
                                'disabled' => false,
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
    // ... more configuration
];
Copied!

Deprecation logging can also be enabled in the additional.php configuration file, here with safeguarding to only enable it in development context:

config/system/additional.php | typo3conf/system/additional.php
<?php

use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

if (Environment::getContext()->isDevelopment()) {
    $GLOBALS['TYPO3_CONF_VARS']['LOG']['TYPO3']['CMS']['deprecations']
    ['writerConfiguration'][LogLevel::NOTICE][FileWriter::class]
    ['disabled'] = false;
}
Copied!

For more information on how to configure the writing of deprecation logs see Writer configuration.

Find calls to deprecated functions 

The extension scanner provides an interactive interface to scan extension code for usage of removed or deprecated TYPO3 Core API.

It is also possible to do a file search for @deprecated and E_USER_DEPRECATED . Using an IDE you can find all calls to the affected methods.

The deprecations are also listed in the changelog of the corresponding TYPO3 version.

Deprecate functions in extensions 

Methods that will be removed in future versions of your extension should be marked as deprecated by both the doc comment and a call to the PHP error method:

Excerpt of EXT:my_extension/Classes/MyClass.php
/**
 * @deprecated since version 3.0.4, will be removed in version 4.0.0
 */
public function decreaseColPosCountByRecord(array $record, int $dec = 1): int
{
    trigger_error(
        'Method "decreaseColPosCountByRecord" is deprecated since version 3.0.4, will be removed in version 4.0.0',
        E_USER_DEPRECATED
    );

    // ... more logic
}
Copied!

For more information about how to deprecate classes, arguments and hooks and how the TYPO3 Core handles deprecations, see How to deprecate classes, methods, arguments and hooks in the TYPO3 core.

Environment 

The TYPO3 Core includes an environment class that contains all environment-specific information, mostly paths within the filesystem. This implementation replaces previously used global variables and constants like PATH_site that have been removed with TYPO3 v10.

The fully qualified class name is \TYPO3\CMS\Core\Core\Environment . The class provides static methods to access the necessary information.

To simulate environments in testing scenarios, the initialize()-method can be called to adjust the information.

Environment PHP API 

getProjectPath() 

The environment provides the path to the folder containing the composer.json. For projects without Composer setup, this is equal to getPublicPath().

getPublicPath() 

The environment provides the path to the public web folder with index.php for the TYPO3 frontend. This was previously PATH_site. For projects without Composer setup, this is equal to getProjectPath().

getVarPath() 

The environment provides the path to the var/ folder. This folder contains data like logs, sessions, locks, and cache files.

For Composer-based installations, it returns var/, in Classic mode installations typo3temp/var/.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/var/`
// Classic mode installations: '/path/to/my-project/typo3temp/var/'
$pathToLabels = Environment::getVarPath();
Copied!

getConfigPath() 

In Composer-based installation this method provides the path config/, in Classic mode installations typo3conf/.

The directory returned by this method contains the folders system/ containing the configuration files system/settings.php and system/additional.php and the folder sites/ containing the site configurations.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/config/system/settings.php`
// Classic mode installations: '/path/to/my-project/typo3conf/system/settings.php'
$pathToSetting = Environment::getConfigPath() . 'system/settings.php';

// Composer-based installations: '/path/to/my-project/config/sites/mysite/config.yaml`
// Classic mode installations: '/path/to/my-project/typo3conf/sites/mysite/config.yaml'
$pathToSiteConfig = Environment::getConfigPath() . 'sites/' . $siteKey . '/config.yaml';
Copied!

getLabelsPath() 

The environment provides the path to var/labels/ in Composer-based installations, respective typo3conf/l10n/ folder in Classic mode installations. This folder contains downloaded translation files.

use TYPO3\CMS\Core\Core\Environment;

// Composer-based installations: '/path/to/my-project/var/labels/`
// Classic mode installations: '/path/to/my-project/typo3conf/l10n/'
$pathToLabels = Environment::getLabelsPath();
Copied!

getCurrentScript() 

Returns the path and filename to the current PHP script.

getContext() 

Returns the current Application Context, usually defined via the TYPO3_CONTEXT environment variable. May be one of Production, Testing, or Development with optional sub-contexts like Production/Staging.

Example, test for production context:

config/system/additional.php | typo3conf/system/additional.php
use TYPO3\CMS\Core\Core\Environment;

$applicationContext = Environment::getContext();
if ($applicationContext->isProduction()) {
   // do something only when in production context
}
Copied!

Error and exception handling 

TYPO3 has a built-in error and exception handling system. Administrators can configure how errors and exceptions are displayed in both the backend and the frontend.

Configuration 

Via the GUI 

You can configure the most important settings for live or debug error handling in the presets:

Admin Tools > Settings > Configuration Presets > Debug Settings

Enable the debug settings in the Admin Tools

For more fine-grained error handling you can change various settings in:

Admin Tools > Settings > Configure Installation-Wide Options > SYS

Via configuration files 

It is also possible to write changes manually into the configuration file config/system/settings.php or config/system/additional.php. Most configuration options related to error and exception handling are part of $GLOBALS['TYPO3_CONF_VARS']['SYS'] .

The following configuration values are of interest:

debug
If enabled, the login refresh is disabled and pageRenderer is set to debug mode. Furthermore the fieldname is appended to the label of fields.
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
If enabled, the total parse time of the page is added as HTTP response header X-TYPO3-Parsetime.
devIPmask
Defines a list of IP addresses which will allow development output to display. Setting to "*" will allow all. Setting it to an empty string allows none.
displayErrors
Configures whether PHP errors or Exceptions should be displayed.
errorHandler
Classname to handle PHP errors. Leave empty to disable error handling.
errorHandlerErrors
The E_* constants that will be handled by the error handler.
exceptionalErrors
The E_* constant that will be converted into an exception by the default errorHandler.
productionExceptionHandler
The default exception handler displays a nice error message when something goes wrong. The error message is logged to the configured logs.
debugExceptionHandler
The default debug exception handler displays the complete stack trace of any encountered exception. The error message and the stack trace is logged to the configured logs.
belogErrorReporting
Configures which PHP errors should be logged to the sys_log table.

Exception handler for rendering TypoScript content objects 

Exceptions which occur during rendering of content objects (typically plugins) will be caught by default in production context and an error message is shown along with the rendered output. For more information and examples have a look into the TypoScript reference for config.contentObjectExceptionHandler.

Error Handler 

Class \TYPO3\CMS\Core\Error\ErrorHandler is the default error handler in TYPO3.

Functions:

  • Can be registered for all PHP errors or for only a subset of the PHP errors which will then be handled by an error handler.
  • Displays error messages as flash messages in the Backend (if exceptionHandler is set to \TYPO3\CMS\Core\Error\DebugExceptionHandler ). Since flash messages are integrated in the Backend template, PHP messages will not destroy the Backend layout.
  • Displays errors as TsLog messages in the adminpanel.
  • Logs error messages via the logging API.
  • Logs error messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will work only with an existing DB connection.

Production exception handler 

Functionality of the \TYPO3\CMS\Core\Error\ProductionExceptionHandler :

  • Shows brief exception message ("Oops, an error occurred!") using \TYPO3\CMS\Core\Controller\ErrorPageController and its attendant template.
  • Logs exception messages via the logging API.
  • Logs exception messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will only work with an existing DB connection.

Depending on the Logging writer configuration the exception output can be found for example in the following locations:

\TYPO3\CMS\Core\Log\Writer\FileWriter
In Composer-based installations the information can be found in directory var/logs/. In Classic mode installations in typo3temp/var/logs/.
\TYPO3\CMS\Core\Log\Writer\SyslogWriter
Logs exception messages to the sys_log table. Logged errors are displayed in the backend module Admin Tools > Log.

Here you find a complete list of Log writers.

Message "Oops, an error occurred!" 

The generic error message "Oops, an error occurred!" is displayed when an exception or error happens within a TypoScript content object like FLUIDTEMPLATE or a plugin. When the exception affects only one content element or plugin it is displayed in place of that elements. However, if it affects the content element representing the whole page like PAGEVIEW only a plain page with this text on it is displayed.

This message is displayed in production context instead of a more detailed exception message. The detailed message can then be found in the log.

Show detailed exception output 

When the frontend debugging is activated, a detailed exception message is output instead of the generic "Oops, an error occurred!" message.

By default, debugging is enabled in the TYPO3 contexts starting with Development. It can also be enabled by setting config.contentObjectExceptionHandler in TypoScript.

Example: prevent "Oops, an error occurred!" messages for logged-in admins 

EXT:my_extension/Configuration/TypoScript/setup.typoscript
[backend.user.isAdmin]
    config.contentObjectExceptionHandler = 0
[END]
Copied!

Debug exception handler 

Functions of \TYPO3\CMS\Core\Error\DebugExceptionHandler :

  • Shows detailed exception messages and full trace of an exception.
  • Logs exception messages via the TYPO3 logging framework.
  • Logs exception messages to the sys_log table. Logged errors are displayed in the belog extension (Admin Tools > Log). This will work only if there is an existing DB connection.

Examples 

Debugging and development setup 

Very verbose configuration which logs and displays all errors and exceptions.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => 1,
   'devIPmask' => '*',
   'errorHandler' => 'TYPO3\\CMS\\Core\\Error\\ErrorHandler',
   'errorHandlerErrors' => E_ALL ^ E_NOTICE,
   'exceptionalErrors' => E_ALL ^ E_NOTICE ^ E_WARNING ^ E_USER_ERROR ^ E_USER_NOTICE ^ E_USER_WARNING,
   'debugExceptionHandler' => 'TYPO3\\CMS\\Core\\Error\\DebugExceptionHandler',
   'productionExceptionHandler' => 'TYPO3\\CMS\\Core\\Error\\DebugExceptionHandler',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

You can also use the "Debug" preset in the Settings module "Configuration presets".

In .htaccess

.htaccess
php_flag display_errors on
php_flag log_errors on
php_value error_log /path/to/php_error.log
Copied!
EXT:some_extension/Configuration/TypoScript/setup.typoscript
config.contentObjectExceptionHandler = 0
Copied!

Use this setting, to get more context and a stacktrace in the Frontend in case of an exception.

See contentObjectExceptionHandler for more information.

Production setup 

Example for a production configuration which displays only errors and exceptions, if the devIPmask setting matches. Errors and exceptions are only logged, if their log level is at least \Psr\Log\LogLevel::WARNING.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => -1,
   'devIPmask' => '[your.IP.address]',
   'errorHandler' => 'TYPO3\\CMS\\Core\\Error\\ErrorHandler',
   'belogErrorReporting' => '0',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

You can also use the "Live" preset in the Settings module "Configuration presets".

In .htaccess:

.htaccess
php_flag display_errors off
php_flag log_errors on
php_value error_log /path/to/php_error.log
Copied!

Performance setup 

Since the error and exception handling and also the logging need some performance, here's an example how to disable error and exception handling completely.

In config/system/settings.php or config/system/additional.php:

config/system/additional.php | typo3conf/system/additional.php
 $changeSettings['SYS'] = [
   'displayErrors' => 0,
   'devIPmask' => '',
   'errorHandler' => '',
   'debugExceptionHandler' => '',
   'productionExceptionHandler' => '',
   'belogErrorReporting' => '0',
];

$GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], $changeSettings);
Copied!

In .htaccess:

.htaccess
php_flag display_errors off
php_flag log_errors off
Copied!

How to extend the error and exception handling 

If you want to register your own error or exception handler:

  1. Create a corresponding class in your extension
  2. Override the Core defaults for productionExceptionHandler, debugExceptionHandler or errorHandler in config/system/additional.php:

    config/system/additional.php | typo3conf/system/additional.php
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandler'] = \Vendor\Ext\Error\MyOwnErrorHandler::class;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] = \Vendor\Ext\Error\MyOwnDebugExceptionHandler::class;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler'] = \Vendor\Ext\Error\MyOwnProductionExceptionHandler::class;
    Copied!

An error or exception handler class must register an error (exception) handler in its constructor. Have a look at the files in EXT:core/Classes/Error/ to see how this should be done.

If you want to use the built-in error and exception handling but extend it with your own functionality, derive your class from the error and exception handling classes shipped with TYPO3.

Example Debug Exception Handler 

This uses the default Core exception handler DebugExceptionHandler and overrides some of the functionality:

EXT:some_extension/Classes/Error/PostExceptionsOnTwitter.php
namespace Vendor\SomeExtension\Error;

class PostExceptionsOnTwitter extends \TYPO3\CMS\Core\Error\DebugExceptionHandler
{
    public function echoExceptionWeb(Exception $exception)
    {
        $this->postExceptionsOnTwitter($exception);
    }

    public function postExceptionsOnTwitter($exception)
    {
        // do it ;-)
    }
}
Copied!
config/system/additional.php | typo3conf/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['debugExceptionHandler'] = \Vendor\SomeExtension\Error\PostExceptionsOnTwitter::class;
Copied!

Extending the TYPO3 Core 

Events and hooks provide an easy way to extend the functionality of the TYPO3 Core and its extensions without blocking others to do the same.

Events are being emitted by the TYPO3 Core or an extension via the event dispatcher. The event will be received by all implemented event listeners for the event in question. Events are strongly typed. Events only allow changes to variables that are intended to be changed by the event.

Hooks are basically places in the source code where a user function will be called for processing, if such has been configured.

Changed in version 12.0

Signals and slots and all related classes have been removed from the Core. Use PSR-14 events instead.

TYPO3 extending mechanisms video 

Lina Wolf: Extending Extensions @ TYPO3 Developer Days 2019

Events and hooks vs. XCLASS extensions 

Events and hooks are the recommended way of extending TYPO3 compared to extending PHP classes with a child class (see XCLASS extensions). Using the XCLASS functionality only one extension of a PHP class can exist at a time while hooks and events may allow many different user-designed processor functions to be executed. With TYPO3 v10 the event dispatcher was introduced. It is a strongly typed way of extending TYPO3 and therefore recommended to use wherever available.

However, events have to be emitted and hooks have to be implemented in the TYPO3 Core or an extension before they can be used, while extending a PHP class via the XCLASS method allows you to extend any class you like.

Proposing events 

If you need to extend the functionality of a class which has no event or hook yet, then you should suggest emitting an event. Normally that is rather easily done by the author of the source you want to extend:

  • For the TYPO3 Core create an issue on forge.typo3.org.
  • For a third-party extension create an issue in the according issue tracker of that extension.

Event dispatcher (PSR-14 events) 

The event dispatcher system was added to extend TYPO3's Core behaviour in TYPO3 v10.0. In the past, this was done via Extbase's signal/slot and TYPO3's custom hook system. The event dispatcher system is a fully-capable replacement for new code in TYPO3, as well as a possibility to migrate away from previous TYPO3 solutions.

Don't get hooked, listen to events! PSR-14 within TYPO3 v10.

-- Benni Mack @ TYPO3 Developer Days 2019

For a basic example on listening to an event, see the chapter Listen to an event in the extension development how-to section.

Quick start 

Dispatching an event 

This quick start section shows how to create your own event class and dispatch it. If you just want to listen on an existing event, see section Implementing an event listener in your extension.

  1. Create an event class.

    An event class is basically a plain PHP object with getters for immutable properties and setters for mutable properties. It contains a constructor for all properties:

    EXT:my_extension/Classes/Event/DoingThisAndThatEvent.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Event;
    
    final class DoingThisAndThatEvent
    {
        public function __construct(
            private string $mutableProperty,
            private readonly int $immutableProperty,
        ) {}
    
        public function getMutableProperty(): string
        {
            return $this->mutableProperty;
        }
    
        public function setMutableProperty(string $mutableProperty): void
        {
            $this->mutableProperty = $mutableProperty;
        }
    
        public function getImmutableProperty(): int
        {
            return $this->immutableProperty;
        }
    }
    
    Copied!

    Read more about implementing event classes.

  2. Inject the event dispatcher

    If you are in a controller, the event dispatcher has already been injected, and in this case you can omit this step.

    If the event dispatcher is not yet available, you need to inject it:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    }
    
    Copied!
  3. Dispatch the event

    Create an event object with the data that should be passed to the listeners. Use the data of mutable properties as it suits your business logic:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use MyVendor\MyExtension\Event\DoingThisAndThatEvent;
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    
        public function doSomething(): void
        {
            // ..
    
            /** @var DoingThisAndThatEvent $event */
            $event = $this->eventDispatcher->dispatch(
                new DoingThisAndThatEvent('foo', 2),
            );
            $someChangedValue = $event->getMutableProperty();
    
            // ...
        }
    }
    
    Copied!

Description of PSR-14 in the context of TYPO3 

PSR-14 is a lean solution that builds upon wide-spread solutions for hooking into existing PHP code (Frameworks, CMS, and the like).

PSR-14 consists of the following four components:

The event dispatcher object 

The EventDispatcher object is used to trigger an event. TYPO3 has a custom event dispatcher implementation. In PSR-14 all event dispatchers of all frameworks are implementing \Psr\EventDispatcher\EventDispatcherInterface , thus it is possible to replace the event dispatcher with another. The EventDispatcher's main method dispatch() is called in TYPO3 Core or extensions. It receives a PHP object which will then be handed to all available listeners.

The listener provider 

A ListenerProvider object that contains all listeners which have been registered for all events. TYPO3 has a custom listener provider that collects all listeners during compile time. This component is only used internally inside of TYPO3's Core Framework.

The events 

An Event object can be any PHP object and is called from TYPO3 Core or an extension ("emitter") containing all information to be transported to the listeners. By default, all registered listeners get triggered by an event, however, if an event has the interface \Psr\EventDispatcher\StoppableEventInterface implemented, a listener can stop further execution of other event listeners. This is especially useful, if the listeners are candidates to provide information to the emitter. This allows to finish event dispatching, once this information has been acquired.

If an event allows modifications, appropriate methods should be available, although due to PHP's nature of handling objects and the PSR-14 listener signature, it cannot be guaranteed to be immutable.

The listeners 

Extensions and PHP packages can add listeners that are registered via YAML. They are usually associated to Event objects by the fully-qualified class name of the event to be listened on. It is the task of the listener provider to provide configuration mechanisms to represent this relationship.

If multiple event listeners for a specific event are registered, their order can be configured or an existing event listener can also be overridden with a different one.

The System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel) reveals an overview of all registered event listeners, see Debugging event handling.

Advantages of the event dispatcher over hooks 

The main benefits of the event dispatcher approach over hooks is an implementation which helps extension authors to better understand the possibilities by having a strongly typed system based on PHP. In addition, it serves as a bridge to also incorporate other events provided by frameworks that support PSR-14.

Impact on TYPO3 Core development in the future 

TYPO3's event dispatcher serves as the basis to replace all hooks in the future. However, for the time being, hooks work the same way as before, unless migrated to an event dispatcher-like code, whereas a PHP E_USER_DEPRECATED error can be triggered.

Some hooks might not be replaced 1:1 to event dispatcher, but rather superseded with a more robust or future-proof API.

Implementing an event listener in your extension 

New in version 13.0

The event listener class 

An example listener, which hooks into the Mailer API to modify mailer settings to not send any emails, could look like this:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer',
    before: 'someIdentifier, anotherIdentifier',
)]
final readonly class MailerEventListener
{
    public function __invoke(AfterMailerSentMessageEvent $event): void
    {
        // do something
    }
}
Copied!

An extension can define multiple listeners. The attribute can be used on class and method level. The PHP attribute is repeatable, which allows to register the same class to listen for different events.

Once the emitter is triggering an event, this listener is called automatically. Be sure to inspect the event's PHP class to fully understand the capabilities provided by an event.

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener supports the following properties (which are all optional):

identifier
A unique identifier should be declared which identifies the event listener, and orderings can be build upon the identifier. If this property is not explicitly defined, the service name is automatically used instead.
before
This property allows a custom sorting of registered listeners. The listener is then dispatched before the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
after
This property allows a custom sorting of registered listeners. The listener is then dispatched after the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
event
The fully-qualified class name (FQCN) of the dispatched event, that the listener wants to react to. It is recommended to not specify this property, but to use the FQCN as type declaration of the argument within the dispatched method (usually __invoke(EventName $event)).
method
The method to be called. If this property is not given, the listener class is treated as invokable, thus its __invoke() method is called.

The PHP attribute is repeatable, which allows to register the same class to listen for different events, for example:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer-initialization',
    event: AfterMailerSentMessageEvent::class,
)]
#[AsEventListener(
    identifier: 'my-extension/null-mailer-sent-message',
    event: BeforeMailerSentMessageEvent::class,
)]
final readonly class MailerEventListener
{
    public function __invoke(
        AfterMailerSentMessageEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        // do something
    }
}
Copied!

The PHP attribute can also be used on a method level. The above example can also be written as:

EXT:my_extension/Classes/EventListener/MailerEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

final readonly class MailerEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-initialization',
        event: AfterMailerSentMessageEvent::class,
    )]
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-sent-message',
        event: BeforeMailerSentMessageEvent::class,
    )]
    public function __invoke(
        AfterMailerSentMessageEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        // do something
    }
}
Copied!

Registering the event listener via Services.yaml 

New in version 13.0

If using the PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener to configure an event listener, the registration in the Configuration/Services.yaml file is not necessary anymore.

If an extension author wants to provide a custom event listener, an according entry with the tag event.listener can be added to the Configuration/Services.yaml file of that extension.

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        method: handleEvent
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

The tag name event.listener identifies that a listener should be registered.

The custom PHP class \MyVendor\MyExtension\EventListener\NullMailer serves as the listener whose handleEvent() method is called, once the event is dispatched. The name of the listened event is specified as a typed argument to that dispatch method. handleEvent(\TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent $event) will for example listen on the event BeforeMailerSentMessageEvent.

The identifier is a common name, so orderings can be built upon the identifier, the optional before and after attributes allow for custom sorting against the identifier of other listeners. If no identifier is specified, the service name (usually the fully-qualified class name of the listener) is automatically used.

If no attribute method is given, the class is treated as invokable, thus its __invoke() method will be called:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

Overriding event listeners 

Existing event listeners can be overridden by custom implementations. This can be performed with both methods, either by using the PHP #[AsEventListener] attribute, or via EXT:my_extension/Configuration/Services.yaml.

For example, a third-party extension listens on the event \TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent via the PHP attribute:

EXT:some_extension/Classes/EventListener/SeoEvent.php
<?php

declare(strict_types=1);

namespace SomeVendor\SomeExtension\EventListener;

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

#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class SeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code...
    }
}
Copied!

or via Services.yaml declaration:

EXT:some_extension/Configuration/Services.yaml
SomeVendor\SomeExtension\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      identifier: 'ext-some-extension/modify-hreflang'
      after: 'typo3-seo/hreflangGenerator'
Copied!

If you want to replace this event listener with your custom implementation, your extension can achieve this by specifying the overridden identifier via the PHP attribute:

EXT:my_extension/Configuration/Classes/EventListener/MySeoEvent.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

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

// Important: Use the 'identifier' of the original event here to be replaced!
#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class MySeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code which overrides the
        // original EXT:some-extension listener ...
    }
}
Copied!

or via Services.yaml declaration:

EXT:my_extension/Configuration/Services.yaml
# Provide your custom event class:
MyVendor\MyExtension\EventListener\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      # Use the same identifier of the extension that you override!
      identifier: 'ext-some-extension/modify-hreflang'
Copied!

Make sure that you set the identifier property to exactly the string which the original implementation uses. If the identifier is not mentioned specifically in the original implementation, the service name (when unspecified, the fully-qualified name of the event listener class) is used. You can inspect that identifier in the System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel), see Debugging event handling. In this example, if identifier: 'ext-some-extension/modify-hreflang' is not defined, the identifier will be set to identifier: 'SomeVendor\SomeExtension\Seo\HrefLangEventListener' and you could use that identifier in your implementation.

Best practices 

  • When configuring listeners, it is recommended to add one listener class per event type, and have it called via __invoke().
  • When creating a new event PHP class, it is recommended to add an Event suffix to the PHP class, and to move it into an appropriate folder like Classes/Event/ to easily discover events provided by a package. Be careful about the context that should be exposed.
  • The same applies to creating a new event listener PHP class: Add an Listener suffix to the PHP class, and move it to a folder Classes/EventListener/.
  • Emitters (TYPO3 Core or extension authors) should always use Dependency Injection to receive the event dispatcher object as a constructor argument, where possible, by adding a type declaration for \Psr\EventDispatcher\EventDispatcherInterface .
  • A unique and descriptive identifier should be used for event listeners.

Any kind of event provided by TYPO3 Core falls under TYPO3's Core API deprecation policy, except for its constructor arguments, which may vary. Events that should only be used within TYPO3 Core, are marked as @internal, just like other non-API parts of TYPO3. Events marked as @internal should be avoided whenever technically possible.

Debugging event handling 

A complete list of all registered event listeners can be viewed in the the module System > Configuration > Event Listeners (PSR-14). The lowlevel system extension has to be installed for this module to be available.

List of event listeners in the Configuration module

List of event listeners in the Configuration module

To debug all events that are actually dispatched during a frontend request you can use the admin panel:

Go to Admin Panel > Debug > Events and see all dispatched events. The adminpanel system extension has to be installed for this module to be available.

List of dispatched events in the Admin Panel

List of dispatched events in the Admin Panel

Event list 

The following list contains PSR-14 events in the TYPO3 Core .

Contents:

Backend 

The following list contains PSR-14 events in EXT:backend.

Contents:

AfterBackendPageRenderEvent 

New in version 12.0

The PSR-14 event AfterBackendPageRenderEvent has been introduced which serves as a direct replacement for the removed hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPreProcess']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPostProcess']

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent gets triggered after the page in the backend is rendered and includes the rendered page body. Listeners may overwrite the page string if desired.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/after-backend-page-render',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterBackendPageRenderEvent $event): void
    {
        $content = $event->getContent() . ' I was here';
        $event->setContent($content);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterBackendPageRenderEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent

This event triggers after a page has been rendered.

Listeners may update the page content string with a modified version if appropriate.

getContent ( )
Returns
string
setContent ( string $content)
param $content

the content

getView ( )
Returns
\TYPO3\CMS\Core\View\ViewInterface

AfterFormEnginePageInitializedEvent 

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent is available to listen for after the form engine has been initialized (all data has been persisted).

Example 

API 

class AfterFormEnginePageInitializedEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent

Event to listen to after the form engine has been initialized (= all data has been persisted)

getController ( )
Returns
\TYPO3\CMS\Backend\Controller\EditDocumentController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

AfterHistoryRollbackFinishedEvent 

The PSR-14 event \TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent is fired after a history record rollback finished.

Example 

API 

class AfterHistoryRollbackFinishedEvent
Fully qualified name
\TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent

This event is fired after a history record rollback finished.

getRecordHistoryRollback ( )
Returns
\TYPO3\CMS\Backend\History\RecordHistoryRollback
getRollbackFields ( )
Returns
string
getDiff ( )
Returns
array
getDataHandlerInput ( )
Returns
array
getBackendUserAuthentication ( )
Returns
?\TYPO3\CMS\Core\Authentication\BackendUserAuthentication

AfterPageColumnsSelectedForLocalizationEvent 

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent is available to listen for after the form engine has been initialized (and all data has been persisted). It will be dispatched after records and columns are collected in the \TYPO3\CMS\Backend\Controller\Page\LocalizationController .

The event receives:

  • The default columns and columns list built by LocalizationController
  • The list of records that were analyzed to create the columns manifest
  • The parameters received by the LocalizationController

The event allows changes to:

  • the columns
  • the columns list

This allows third-party code to read or manipulate the "columns manifest" that gets displayed in the translation modal when a user has clicked the Translate button in the page module, by implementing a listener for the event.

Example 

API 

class AfterPageColumnsSelectedForLocalizationEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent

This event triggers after the LocalizationController (AJAX) has selected page columns to be translated. Allows third parties to add to or change the columns and content elements withing those columns which will be available for localization through the "translate" modal in the page module.

getColumns ( )

Returns list of columns, indexed by column position number, value is label (either LLL: or hardcoded).

Returns
array
setColumns ( array $columns)
param $columns

the columns

getColumnList ( )

Returns a list of integer column position numbers used in the BackendLayout.

Returns
array
setColumnList ( array $columnList)
param $columnList

the columnList

getBackendLayout ( )
Returns
\TYPO3\CMS\Backend\View\BackendLayout\BackendLayout
getRecords ( )

Returns an array of records which were used when building the original column manifest and column position numbers list.

Returns
array
getParameters ( )

Returns request parameters passed to LocalizationController.

Returns
array

AfterPagePreviewUriGeneratedEvent 

New in version 12.0

This PSR-14 event replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] postProcess hook.

The \TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent is executed in \TYPO3\CMS\Backend\Routing->buildUri(), after the preview URI has been built - or set by an event listener to BeforePagePreviewUriGeneratedEvent. It allows to overwrite the built preview URI. However, this event does not feature the possibility to modify the parameters, since this will not have any effect, as the preview URI is directly returned after event dispatching and no further action is done by the \TYPO3\CMS\Backend\Routing\PreviewUriBuilder .

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-preview-uri',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterPagePreviewUriGeneratedEvent $event): void
    {
        // Add custom fragment to built preview URI
        $uri = $event->getPreviewUri();
        $uri = $uri->withFragment('#customFragment');
        $event->setPreviewUri($uri);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterPagePreviewUriGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent

Listeners to this event will be able to modify the page preview URI, which had been generated for a page in the frontend.

setPreviewUri ( \Psr\Http\Message\UriInterface $previewUri)
param $previewUri

the previewUri

getPreviewUri ( )
Returns
\Psr\Http\Message\UriInterface
getPageId ( )
Returns
int
getLanguageId ( )
Returns
int
getRootline ( )
Returns
array
getSection ( )
Returns
string
getAdditionalQueryParameters ( )
Returns
array
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getOptions ( )
Returns
array

AfterPageTreeItemsPreparedEvent 

New in version 12.0

This PSR-14 event replaces the following hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Workspaces\Service\WorkspaceService']['hasPageRecordVersions']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Workspaces\Service\WorkspaceService']['fetchPagesWithVersionsInTable']

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent allows prepared page tree items to be modified.

It is dispatched in the \TYPO3\CMS\Backend\Controller\Page\TreeController class after the page tree items have been resolved and prepared. The event provides the current PSR-7 request object as well as the page tree items. All items contain the corresponding page record in the special _page key.

Labels 

New in version 13.1

Tree node labels can be defined which offer customizable color markings for tree nodes. They also require an associated label for improved accessibility. Each node can support multiple labels, sorted by priority, with the highest priority label taking precedence over others.

A label can also be assigned to a node via user TSconfig. Please note that only the marker for the label with the highest priority is rendered. All additional labels will only be added to the title of the node.

Status information 

New in version 13.1

Tree nodes can incorporate status information. These details serve to indicate the status of nodes and provide supplementary information. For instance, if a page undergoes changes within a workspace, it will display an indicator on the respective tree node. Additionally, the status is appended to the node's title. This not only improves visual clarity but also enhances information accessibility.

Each node can accommodate multiple status information, prioritized by severity and urgency. Critical messages take precedence over other status notifications.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

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

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-tree-items',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterPageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();
        foreach ($items as &$item) {
            if (($item['_page']['pid'] ?? null) === 123) {
                // Set special icon for page with ID 123
                $item['icon'] = 'my-special-icon';

                // Set a tree node label
                $item['labels'][] = new Label(
                    label: 'Campaign B',
                    color: '#00658f',
                    priority: 1,
                );

                // Set a status information
                $item['statusInformation'][] = new StatusInformation(
                    label: 'A warning message',
                    severity: ContextualFeedbackSeverity::WARNING,
                    priority: 0,
                    icon: 'actions-dot',
                    overlayIcon: '',
                );
            }
        }
        $event->setItems($items);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterPageTreeItemsPreparedEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent

Listeners to this event will be able to modify the prepared page tree items for the page tree

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getItems ( )
Returns
array
setItems ( array $items)
param $items

the items

AfterRawPageRowPreparedEvent 

New in version 13.3

This event was introduced to replace the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections'] which has already been removed with TYPO3 v9.

The PSR-14 event \TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent allows to modify the populated properties of a page and children records before the page is displayed in a page tree.

This can be used, for example, to change the title of a page or apply a different sorting to its children.

Example: Sort pages by title in the page tree 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterRawPageRowPreparedEvent $event): void
    {
        $rawPage = $event->getRawPage();
        if ((int)$rawPage['uid'] === 123) {
            // Sort pages alphabetically in the page tree
            $rawPage['_children'] = usort(
                $rawPage['_children'],
                static fn(array $a, array $b) => strcmp($a['title'], $b['title']),
            );
            $rawPage['title'] = 'Some special title';
            $event->setRawPage($rawPage);
        }
    }
}
Copied!

API of AfterRawPageRowPreparedEvent 

class AfterRawPageRowPreparedEvent
Fully qualified name
\TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent

Listeners to this event will be able to modify a page with the special _children key, or completely change e.g. a title.

getRawPage ( )
Returns
array
setRawPage ( array $rawPage)
param $rawPage

the rawPage

getWorkspaceId ( )
Returns
int

AfterRecordSummaryForLocalizationEvent 

New in version 12.0

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent is fired in the \TYPO3\CMS\Backend\Controller\Page\RecordSummaryForLocalization class and allows extensions to modify the payload of the JsonResponse.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/after-record-summary-for-localization',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterRecordSummaryForLocalizationEvent $event): void
    {
        // Get current records
        $records = $event->getRecords();

        // ... do something with $records

        // Set new records
        $event->setRecords($records);

        // Get current columns
        $columns = $event->getColumns();

        // ... do something with $columns

        // Set new columns
        $event->setColumns($columns);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterRecordSummaryForLocalizationEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent
getColumns ( )
Returns
array
setColumns ( array $columns)
param $columns

the columns

getRecords ( )
Returns
array
setRecords ( array $records)
param $records

the records

AfterSectionMarkupGeneratedEvent 

The PSR-14 event \TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent allows extension authors to display content in any colPos after the last content element.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/after-section-markup-generated',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterSectionMarkupGeneratedEvent $event): void
    {
        // Check for relevant backend layout
        if ($event->getPageLayoutContext()->getBackendLayout()->getIdentifier() !== 'someBackendLayout') {
            return;
        }

        // Check for relevant column
        if ($event->getColumnConfig()['identifier'] !== 'someColumn') {
            return;
        }

        $event->setContent('
            <div class="t3-page-ce">
                <div class="t3-page-ce-element">
                    <div class="t3-page-ce-header">
                        <div class="t3-page-ce-header-title">
                            Some content at the end of the column
                        </div>
                    </div>
                </div>
            </div>
        ');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterSectionMarkupGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent

This event can be triggered to display content in any colpos after the last content element.

getColumnConfig ( )
Returns
array
getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getRecords ( )
Returns
array
setContent ( string $content = '')
param $content

the content, default: ''

getContent ( )
Returns
string
isPropagationStopped ( )

Prevent other listeners from being called if rendering is stopped by listener.

Returns
bool
setStopRendering ( bool $stopRendering)
param $stopRendering

the stopRendering

BeforeFormEnginePageInitializedEvent 

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent allows to listen for before the form engine has been initialized (before all data will be persisted).

Example 

API 

class BeforeFormEnginePageInitializedEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent

Event to listen to before the form engine has been initialized (= before all data will be persisted)

getController ( )
Returns
\TYPO3\CMS\Backend\Controller\EditDocumentController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

BeforeHistoryRollbackStartEvent 

The PSR-14 event \TYPO3\CMS\Backend\History\Event\BeforeHistoryRollbackStartEvent is fired before a history record rollback starts.

Example 

API 

class BeforeHistoryRollbackStartEvent
Fully qualified name
\TYPO3\CMS\Backend\History\Event\BeforeHistoryRollbackStartEvent

This event is fired before a history record rollback starts

getRecordHistoryRollback ( )
Returns
\TYPO3\CMS\Backend\History\RecordHistoryRollback
getRollbackFields ( )
Returns
string
getDiff ( )
Returns
array
getBackendUserAuthentication ( )
Returns
?\TYPO3\CMS\Core\Authentication\BackendUserAuthentication

BeforeModuleCreationEvent 

The PSR-14 event \TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent allows extension authors to manipulate the module configuration, before it is used to create and register the module.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-module-icon',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeModuleCreationEvent $event): void
    {
        // Change module icon of page module
        if ($event->getIdentifier() === 'web_layout') {
            $event->setConfigurationValue('iconIdentifier', 'my-custom-icon-identifier');
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BeforeModuleCreationEvent
Fully qualified name
\TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent

Listeners can adjust the module configuration before the module gets created and registered

getIdentifier ( )
Returns
string
getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

the configuration

hasConfigurationValue ( string $key)
param $key

the key

Returns
bool
getConfigurationValue ( string $key, ?mixed $default = NULL)
param $key

the key

param $default

the default, default: NULL

Returns
?mixed
setConfigurationValue ( string $key, ?mixed $value)
param $key

the key

param $value

the value

BeforePagePreviewUriGeneratedEvent 

New in version 12.0

This PSR-14 event replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] preProcess hook.

The \TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent is executed in \TYPO3\CMS\Backend\Routing->buildUri(), before the preview URI is actually built. It allows to either adjust the parameters, such as the page id or the language id, or to set a custom preview URI, which will then stop the event propagation and also prevents \TYPO3\CMS\Backend\Routing\PreviewUriBuilder from building the URI based on the parameters.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-parameters',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforePagePreviewUriGeneratedEvent $event): void
    {
        // Add custom query parameter before URI generation
        $event->setAdditionalQueryParameters(
            array_replace_recursive(
                $event->getAdditionalQueryParameters(),
                ['myParam' => 'paramValue'],
            ),
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BeforePagePreviewUriGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent

Listeners to this event will be able to modify the corresponding parameters, before the page preview URI is being generated, when linking to a page in the frontend.

setPreviewUri ( \Psr\Http\Message\UriInterface $uri)
param $uri

the uri

getPreviewUri ( )
Returns
?\Psr\Http\Message\UriInterface
isPropagationStopped ( )
Returns
bool
getPageId ( )
Returns
int
setPageId ( int $pageId)
param $pageId

the pageId

getLanguageId ( )
Returns
int
setLanguageId ( int $languageId)
param $languageId

the languageId

getRootline ( )
Returns
array
setRootline ( array $rootline)
param $rootline

the rootline

getSection ( )
Returns
string
setSection ( string $section)
param $section

the section

getAdditionalQueryParameters ( )
Returns
array
setAdditionalQueryParameters ( array $additionalQueryParameters)
param $additionalQueryParameters

the additionalQueryParameters

getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getOptions ( )
Returns
array

BeforeRecordDownloadIsExecutedEvent 

New in version 13.2

This PSR-14 event replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , hooks, which have been deprecated with TYPO3 v13.2. See also Migration.

The event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent can be used to modify the result of a download / export initiated via the Web > List module.

The event lets you change both the main part and the header of the data file. You can use it to edit data to follow GDPR rules, change or translate data, create backups or web hooks, record who accesses the data, and more.

Example: Redact columns with private content in exports 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/record-list-download-data')]
final readonly class DataListener
{
    public function __invoke(BeforeRecordDownloadIsExecutedEvent $event): void
    {
        // List of redactable fields.
        $gdprFields = ['title', 'author'];

        $headerRow = $event->getHeaderRow();
        $records = $event->getRecords();

        // Iterate header to mark redacted fields...
        foreach ($headerRow as $headerRowKey => $headerRowValue) {
            if (in_array($headerRowKey, $gdprFields, true)) {
                $headerRow[$headerRowKey] .= ' (REDACTED)';
            }
        }

        // Redact actual content...
        foreach ($records as $index => $record) {
            foreach ($gdprFields as $gdprField) {
                if (isset($record[$gdprField])) {
                    $records[$index][$gdprField] = '(REDACTED)';
                }
            }
        }

        $event->setHeaderRow($headerRow);
        $event->setRecords($records);
    }
}
Copied!

API of BeforeRecordDownloadIsExecutedEvent 

class BeforeRecordDownloadIsExecutedEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent

Listeners to this event are able to manipulate the download of records, usually triggered via Web > List.

getHeaderRow ( )
Returns
array
setHeaderRow ( array $headerRow)
param $headerRow

the headerRow

getRecords ( )
Returns
array
setRecords ( array $records)
param $records

the records

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getTable ( )
Returns
string
getFormat ( )
Returns
string
getFilename ( )
Returns
string
getId ( )
Returns
int
getModTSconfig ( )
Returns
array
getColumnsToRender ( )
Returns
array
isHideTranslations ( )
Returns
bool

Migration 

Deprecated since version 13.2

The previously used hooks $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , used to manipulate the download / export configuration of records, triggered in the Web > List backend module, have been deprecated in favor of a new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent .

Migrating customizeCsvHeader 

The prior hook parameter/variable fields is now available via $event->getColumnsToRender(). The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getHeaderRow().

Migrating customizeCsvRow 

The following prior hook parameters/variables have these substitutes:

databaseRow
is now available via $event->getRecords() (see note below).
tableName
is now available via $event->getTable().
pageId
is now available via $event->getId().

The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getRecords().

Please note that the hook was previously executed once per row retrieved from the database. The PSR-14 event however - due to performance reasons - is only executed for the full record list after database retrieval, thus allows post-processing on this whole dataset.

BeforeRecordDownloadPresetsAreDisplayedEvent 

New in version 13.2

The event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent can be used to manipulate the list of available download presets in the Web > List module.

See mod.web_list.downloadPresets on how to configure download presets.

Note that the event is dispatched for one specific database table name. If an event listener is created to attach presets to different tables, the listener method must check for the table name, as shown in the example below.

If no download presets exist for a given table, the PSR-14 event can still be used to modify and add presets to it via the setPresets() method.

The array passed from getPresets() to setPresets() can contain an array collection of TYPO3CMSBackendRecordListDownloadPreset objects with the array key using the preset label.

The event listener can also remove array indexes, or also columns of existing array entries by passing a newly constructed DownloadPreset object with the changed label and columns constructor properties.

Example: Manipulate download presets 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\RecordList\EventListener;

use TYPO3\CMS\Backend\RecordList\DownloadPreset;
use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-extension/modify-record-list-preset')]
final readonly class PresetListener
{
    public function __invoke(BeforeRecordDownloadPresetsAreDisplayedEvent $event): void
    {
        $presets = $event->getPresets();

        $newPresets = match ($event->getDatabaseTable()) {
            'be_users' => [new DownloadPreset('PSR-14 preset', ['uid', 'email'])],
            'pages' => [
                new DownloadPreset('PSR-14 preset', ['title']),
                new DownloadPreset('Another PSR-14 preset', ['title', 'doktype']),
            ],
            'tx_myvendor_myextension' => [new DownloadPreset('PSR-14 preset', ['uid', 'something'])],
        };

        foreach ($newPresets as $newPreset) {
            $presets[] = $newPreset;
        }

        $presets[] = new DownloadPreset('Available everywhere, simple UID list', ['uid']);
        $presets['some-identifier'] = new DownloadPreset('Overwrite preset', ['uid', 'pid'], 'some-identifier');

        $event->setPresets($presets);
    }
}
Copied!

API of BeforeRecordDownloadPresetsAreDisplayedEvent 

class BeforeRecordDownloadPresetsAreDisplayedEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent

Event to manipulate the available list of download presets.

Array $presets contains a list of DownloadPreset objects with their methods: getIdentifier(), getLabel() and getColumns().

The event is always coupled to a specific database table.

getPresets ( )
Returns
\DownloadPreset[]
setPresets ( array $presets)
param $presets

the presets

getDatabaseTable ( )
Returns
string
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getId ( )
Returns
int

API of DownloadPreset 

class DownloadPreset
Fully qualified name
\TYPO3\CMS\Backend\RecordList\DownloadPreset
getIdentifier ( )
Returns
string
getLabel ( )
Returns
string
getColumns ( )
Returns
array
create ( array $preset)
param $preset

the preset

Returns
self

BeforeSearchInDatabaseRecordProviderEvent 

New in version 12.1

The TYPO3 backend search (also known as "Live Search") uses the \TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider to search for records in database tables, having searchFields configured in TCA.

In some individual cases it may not be desirable to search in a specific table. Therefore, the PSR-14 event \TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent is available, which allows to exclude / ignore such tables by adding them to a deny list. Additionally, the PSR-14 event can be used to restrict the search result on certain page IDs or to modify the search query altogether.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/before-search-in-database-record-provider',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeSearchInDatabaseRecordProviderEvent $event): void
    {
        $event->ignoreTable('my_custom_table');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BeforeSearchInDatabaseRecordProviderEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\BeforeSearchInDatabaseRecordProviderEvent

PSR-14 event to modify the incoming input about which tables should be searched for within the live search results. This allows adding additional DB tables to be excluded / ignored, to further limit the search result on certain page IDs or to modify the search query altogether.

getSearchPageIds ( )
Returns
array
setSearchPageIds ( array $searchPageIds)
param $searchPageIds

the searchPageIds

getSearchDemand ( )
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand
setSearchDemand ( \TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand $searchDemand)
param $searchDemand

the searchDemand

ignoreTable ( string $table)
param $table

the table

setIgnoredTables ( array $tables)
param $tables

the tables

isTableIgnored ( string $table)
param $table

the table

Returns
bool
getIgnoredTables ( )
Returns
string[]

BeforeSectionMarkupGeneratedEvent 

The PSR-14 event \TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent allows extension authors to display content in any colPos before the first content element.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/before-section-markup-generated',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeSectionMarkupGeneratedEvent $event): void
    {
        // Check for relevant backend layout
        if ($event->getPageLayoutContext()->getBackendLayout()->getIdentifier() !== 'someBackendLayout') {
            return;
        }

        // Check for relevant column
        if ($event->getColumnConfig()['identifier'] !== 'someColumn') {
            return;
        }

        $event->setContent('
            <div class="t3-page-ce">
                <div class="t3-page-ce-element">
                    <div class="t3-page-ce-header">
                        <div class="t3-page-ce-header-title">
                            Some content at the start of the column
                        </div>
                    </div>
                </div>
            </div>
        ');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BeforeSectionMarkupGeneratedEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent

This event can be triggered to display content in any colPos before the first content element.

getColumnConfig ( )
Returns
array
getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getRecords ( )
Returns
array
setContent ( string $content = '')
param $content

the content, default: ''

getContent ( )
Returns
string
isPropagationStopped ( )

Prevent other listeners from being called if rendering is stopped by listener.

Returns
bool
setStopRendering ( bool $stopRendering)
param $stopRendering

the stopRendering

CustomFileControlsEvent 

New in version 12.0

This event replaces the customControls hook option, which is only available for TCA type inline.

Listeners to the PSR-14 event \TYPO3\CMS\Backend\Form\Event\CustomFileControlsEvent are able to add custom controls to a TCA type file field in form engine.

Custom controls are always displayed below the file references. In contrast to the selectors, e.g. Select & upload files are custom controls independent of the readonly and showFileSelectors options. This means, you have full control in which scenario your custom controls are being displayed.

Example 

API 

class CustomFileControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\CustomFileControlsEvent

Listeners to this Event will be able to add custom controls to a TCA type="file" field in FormEngine

getResultArray ( )
Returns
array
setResultArray ( array $resultArray)

WARNING: Modifying the result array should be used with care. It mostly only exists to allow additional $resultArray['javaScriptModules'].

param $resultArray

the resultArray

getControls ( )
Returns
array
setControls ( array $controls)
param $controls

the controls

addControl ( string $control, string $identifier = '')
param $control

the control

param $identifier

the identifier, default: ''

removeControl ( string $identifier)
param $identifier

the identifier

Returns
bool
getTableName ( )
Returns
string
getFieldName ( )
Returns
string
getDatabaseRow ( )
Returns
array
getFieldConfig ( )
Returns
array
getFormFieldIdentifier ( )
Returns
string
getFormFieldName ( )
Returns
string

IsContentUsedOnPageLayoutEvent 

New in version 12.0

This event \TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent serves as a drop-in replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] hook.

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent to identify if content has been used in a column that is not in a backend layout.

Setting $event->setUsed(true) prevent the following message for the affected content element, setting it to false displays it:

Example: Display "Unused elements detected on this page" for elements with missing parent 

EXT:my_extension/Classes/Listener/ContentUsedOnPage.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Listener;

use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/view/content-used-on-page',
)]
final readonly class ContentUsedOnPage
{
    public function __invoke(IsContentUsedOnPageLayoutEvent $event): void
    {
        // Get the current record from the event.
        $record = $event->getRecord();

        // This code will be your domain logic to indicate if content
        // should be hidden in the page module.
        if ((int)($record['colPos'] ?? 0) === 999
            && !empty($record['tx_myext_content_parent'])
        ) {
            // Flag the current element as not used. Set it to true, if you
            // want to flag it as used and hide it from the page module.
            $event->setUsed(false);
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API of IsContentUsedOnPageLayoutEvent 

class IsContentUsedOnPageLayoutEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent

Use this Event to identify whether a content element is used.

getRecord ( )
Returns
array
isRecordUsed ( )
Returns
bool
setUsed ( bool $isUsed)
param $isUsed

the isUsed

getKnownColumnPositionNumbers ( )
Returns
array
getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext

IsFileSelectableEvent 

New in version 12.1

The PSR-14 event \TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent allows to decide whether a file can be selected in the file browser.

To get the image dimensions (width and height) of a file, you can retrieve the file and use the getProperty() method.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-file-is-selectable',
)]
final readonly class MyEventListener
{
    public function __invoke(IsFileSelectableEvent $event): void
    {
        // Deny selection of "png" images
        if ($event->getFile()->getExtension() === 'png') {
            $event->denyFileSelection();
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class IsFileSelectableEvent
Fully qualified name
\TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent

Listeners to this event are able to define whether a file can be selected in the file browser

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
isFileSelectable ( )
Returns
bool
allowFileSelection ( )
denyFileSelection ( )

ModifyAllowedItemsEvent 

New in version 12.0

This event has been introduced together with ModifyLinkHandlersEvent to serve as a direct replacement for the following removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] . It replaces the method modifyAllowedItems() in this hook.

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent allows extension authors to add or remove from the list of allowed link types.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/allowed-items',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyAllowedItemsEvent $event): void
    {
        $event->addAllowedItem('someItem');
        $event->removeAllowedItem('anotherItem');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyAllowedItemsEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent

This event allows extensions to add or remove from the list of allowed link types.

getAllowedItems ( )
Returns
string[]
addAllowedItem ( string $item)
param $item

the item

Returns
self
removeAllowedItem ( string $new)
param $new

the new

Returns
self
getCurrentLinkParts ( )
Returns
array<string,mixed>

ModifyButtonBarEvent 

New in version 12.0

This event serves as a direct replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend\Template\Components\ButtonBar']['getButtonsHook'] .

The PSR-14 event \TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent can be used to modify the button bar in the TYPO3 backend module docheader.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-button-bar',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyButtonBarEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyButtonBarEvent
Fully qualified name
\TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent

Listeners can modify the buttons of the button bar in the backend module docheader

getButtons ( )
Returns
\Buttons
setButtons ( array $buttons)
param $buttons

the buttons

getButtonBar ( )
Returns
\TYPO3\CMS\Backend\Template\Components\ButtonBar

ModifyClearCacheActionsEvent 

The PSR-14 event \TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent is fired in the \TYPO3\CMS\Backend\Backend\ToolbarItems\ClearCacheToolbarItem class and allows extension authors to modify the clear cache actions, shown in the TYPO3 backend top toolbar.

The event can be used to change or remove existing clear cache actions, as well as to add new actions. Therefore, the event also contains, next to the usual "getter" and "setter" methods, the convenience method add() for the cacheActions and cacheActionIdentifiers arrays.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

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

#[AsEventListener(
    identifier: 'my-extension/toolbar/my-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyClearCacheActionsEvent $event): void
    {
        // do magic here
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

The cache action array element consists of the following keys and values:

Example cache action array
$event->addCacheAction([
    // Required keys:
    'id' => 'pages',
    'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesTitle',
    'href' => (string)$uriBuilder->buildUriFromRoute('tce_db', ['cacheCmd' => 'pages']),
    'iconIdentifier' => 'actions-system-cache-clear-impact-low',
    // Optional, recommended keys:
    'description' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesDescription',
    'severity' => 'success',
]);
Copied!

The key severity can contain one of these strings: notice, info, success, warning, error.

The cache identifier array is a numerical array in which the array value corresponds to the registered id of the cache action array. Here is an example of how to use it for a custom cache action:

Example cache action array combined with a cache identifier array
$myIdentifier = 'myExtensionCustomStorageCache';
$event->addCacheAction([
    'id' => $myIdentifier,
    'title' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:CacheActionTitle',
    // Note to register your own route, this is an example
    'href' => (string)$uriBuilder->buildUriFromRoute('ajax_' . $myIdentifier . '_purge'),
    'iconIdentifier' => 'actions-system-cache-clear-impact-low',
    'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:CacheActionDescription',
    'severity' => 'notice',
]);
$event->addCacheActionIdentifier($myIdentifier);
Copied!

API 

class ModifyClearCacheActionsEvent
Fully qualified name
\TYPO3\CMS\Backend\Backend\Event\ModifyClearCacheActionsEvent

An event to modify the clear cache actions, shown in the TYPO3 Backend top toolbar

addCacheAction ( array $cacheAction)
param $cacheAction

the cacheAction

setCacheActions ( array $cacheActions)
param $cacheActions

the cacheActions

getCacheActions ( )
Returns
list<\CacheAction>
addCacheActionIdentifier ( string $cacheActionIdentifier)
param $cacheActionIdentifier

the cacheActionIdentifier

setCacheActionIdentifiers ( array $cacheActionIdentifiers)
param $cacheActionIdentifiers

the cacheActionIdentifiers

getCacheActionIdentifiers ( )
Returns
list<non-empty-string>

ModifyDatabaseQueryForContentEvent 

New in version 12.0

This event has been introduced which serves as a drop-in replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] hook.

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent to filter out certain content elements from being shown in the Page module.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Connection;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-database-query-for-content',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyDatabaseQueryForContentEvent $event): void
    {
        // Early return if we do not need to react
        if ($event->getTable() !== 'tt_content') {
            return;
        }

        // Retrieve QueryBuilder instance from event
        $queryBuilder = $event->getQueryBuilder();

        // Add an additional condition to the QueryBuilder for the table
        // Note: This is only a example, modify the QueryBuilder instance
        //       here to your needs.
        $queryBuilder = $queryBuilder->andWhere(
            $queryBuilder->expr()->neq(
                'some_field',
                $queryBuilder->createNamedParameter(1, Connection::PARAM_INT),
            ),
        );

        // Set updated QueryBuilder to event
        $event->setQueryBuilder($queryBuilder);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyDatabaseQueryForContentEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent

Use this Event to alter the database query when loading content for a page.

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
setQueryBuilder ( \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder)
param $queryBuilder

the queryBuilder

getTable ( )
Returns
string
getPageId ( )
Returns
int

ModifyDatabaseQueryForRecordListingEvent 

New in version 12.0

This event has been introduced to replace the following removed hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['modifyQuery']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['makeSearchStringConstraints']

The PSR-14 event \TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent allows to alter the query builder SQL statement before a list of records is rendered in record lists, such as the List module or an element browser.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-database-query-for-record-list',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyDatabaseQueryForRecordListingEvent $event): void
    {
        $queryBuilder = $event->getQueryBuilder();

        // ... do something ...

        $event->setQueryBuilder($queryBuilder);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyDatabaseQueryForRecordListingEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent

Use this Event to alter the database query when loading content for a page (usually in the list module) before it is executed.

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
setQueryBuilder ( \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder)
param $queryBuilder

the queryBuilder

getTable ( )
Returns
string
getPageId ( )
Returns
int
getFields ( )
Returns
array
getFirstResult ( )
Returns
int
getMaxResults ( )
Returns
int
getDatabaseRecordList ( )
Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyEditFormUserAccessEvent 

New in version 12.0

This event serves as a more powerful and flexible alternative for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/alt_doc.php']['makeEditForm_accessCheck'] hook.

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent\ModifyEditFormUserAccessEvent provides the full database row of the record in question next to the exception, which might have been set by the Core. Additionally, the event allows to modify the user access decision in an object-oriented way, using convenience methods.

In case any listener to the new event denies user access, while it was initially allowed by Core, the \TYPO3\CMS\Backend\Form\Exception\AccessDeniedListenerException will be thrown.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-edit-form-user-access',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyEditFormUserAccessEvent $event): void
    {
        // Deny access for creating records of a custom table
        if ($event->getTableName() === 'tx_myext_domain_model_mytable' && $event->getCommand() === 'new') {
            $event->denyUserAccess();
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyEditFormUserAccessEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent

Listeners to this Event will be able to modify the user access decision for using FormEngine to create or edit a record.

allowUserAccess ( )

Allows user access to the editing form

denyUserAccess ( )

Denies user access to the editing form

doesUserHaveAccess ( )

Returns the current user access state

Returns
bool
getAccessDeniedException ( )

If Core's DataProvider previously denied access, this returns the corresponding exception, null otherwise

Returns
?\TYPO3\CMS\Backend\Form\Exception\AccessDeniedException
getTableName ( )

Returns the table name of the record in question

Returns
string
getCommand ( )

Returns the requested command, either new or edit

Returns
"new"|"edit"
getDatabaseRow ( )

Returns the record's database row

Returns
array

ModifyFileReferenceControlsEvent 

New in version 12.0

Listeners to the PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceControlsEvent are able to modify the controls of a single file reference of a TCA type file field. This event is similar to the ModifyInlineElementControlsEvent, which is only available for TCA type inline.

Example 

API 

class ModifyFileReferenceControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceControlsEvent

Listeners to this Event will be able to modify the controls of a single file reference of a TCA type=file field.

getControls ( )

Returns all controls with their markup

Returns
array
setControls ( array $controls)

Overwrite the controls

param $controls

the controls

getControl ( string $identifier)

Returns the markup for the requested control

param $identifier

the identifier

Returns
string
setControl ( string $identifier, string $markup)

Set a control with the given identifier and markup IMPORTANT: Overwrites an existing control with the same identifier

param $identifier

the identifier

param $markup

the markup

hasControl ( string $identifier)

Returns whether a control exists for the given identifier

param $identifier

the identifier

Returns
bool
removeControl ( string $identifier)

Removes a control from the file reference, if it exists

param $identifier

the identifier

Return description

Whether the control could be removed

Returns
bool
getElementData ( )

Returns the whole element data

Returns
array
getRecord ( )

Returns the current record, the controls are created for

Returns
array
getParentUid ( )

Returns the uid of the parent (embedding) record (uid or NEW...)

Returns
string
getForeignTable ( )

Returns the table (foreign_table) the controls are created for

Returns
string
getFieldConfiguration ( )

Returns the TCA configuration of the TCA type=file field

Returns
array
isVirtual ( )

Returns whether the current records is only virtually shown and not physically part of the parent record

Returns
bool

ModifyFileReferenceEnabledControlsEvent 

New in version 12.0

Listeners to the PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceEnabledControlsEvent are able to modify the state (enabled or disabled) for the controls of a single file reference of a TCA type file field. This event is similar to the ModifyInlineElementEnabledControlsEvent, which is only available for TCA type inline.

Example 

API 

class ModifyFileReferenceEnabledControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceEnabledControlsEvent

Listeners to this Event will be able to modify the state (enabled or disabled) for controls of a file reference

enableControl ( string $identifier)

Enable a control, if it exists

param $identifier

the identifier

Return description

Whether the control could be enabled

Returns
bool
disableControl ( string $identifier)

Disable a control, if it exists

param $identifier

the identifier

Return description

Whether the control could be disabled

Returns
bool
hasControl ( string $identifier)

Returns whether a control exists for the given identifier

param $identifier

the identifier

Returns
bool
isControlEnabled ( string $identifier)

Returns whether the control is enabled.

Note: Will also return FALSE in case no control exists for the requested identifier

param $identifier

the identifier

Returns
bool
getControlsState ( )

Returns all controls with their state (enabled or disabled)

Returns
array
getEnabledControls ( )

Returns all enabled controls

Returns
array
getElementData ( )

Returns the whole element data

Returns
array
getRecord ( )

Returns the current record of the controls are created for

Returns
array
getParentUid ( )

Returns the uid of the parent (embedding) record (uid or NEW...)

Returns
string
getForeignTable ( )

Returns the table (foreign_table) the controls are created for

Returns
string
getFieldConfiguration ( )

Returns the TCA configuration of the TCA type=file field

Returns
array
isVirtual ( )

Returns whether the current records is only virtually shown and not physically part of the parent record

Returns
bool

ModifyGenericBackendMessagesEvent 

New in version 12.0

This event serves as direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['displayWarningMessages'] .

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent allows to add or alter messages that are displayed in the About module (default start module of the TYPO3 backend).

Extensions such as the EXT:reports system extension use this event to display custom messages based on the system status:

A generic backend message in the about module

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Messaging\FlashMessage;

#[AsEventListener(
    identifier: 'my-extension/backend/add-message',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyGenericBackendMessagesEvent $event): void
    {
        // Add a custom message
        $event->addMessage(new FlashMessage('My custom message'));
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyGenericBackendMessagesEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyGenericBackendMessagesEvent

Listeners to this event are able to add or change messages for the "Help > About" module.

getMessages ( )
Returns
array
addMessage ( \TYPO3\CMS\Core\Messaging\AbstractMessage $message)
param $message

the message

setMessages ( array $messages)
param $messages

the messages

ModifyImageManipulationPreviewUrlEvent 

New in version 12.0

This event serves as a direct replacement for the now removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend/Form/Element/ImageManipulationElement']['previewUrl'] hook.

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent can be used to modify the preview URL within the image manipulation element, used for example for the crop field of the sys_file_reference table.

As soon as a preview URL is set, the image manipulation element will display a corresponding button in the footer of the modal window, next to the Cancel and Accept buttons. On click, the preview URL will be opened in a new window.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-imagemanipulation-previewurl',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyImageManipulationPreviewUrlEvent $event): void
    {
        $event->setPreviewUrl('https://example.com/some/preview/url');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyImageManipulationPreviewUrlEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent

Listeners to this Event will be able to modify the preview url, used in the ImageManipulation element

getDatabaseRow ( )
Returns
array
getFieldConfiguration ( )
Returns
array
getFile ( )
Returns
\TYPO3\CMS\Core\Resource\File
getPreviewUrl ( )
Returns
string
setPreviewUrl ( string $previewUrl)
param $previewUrl

the previewUrl

ModifyInlineElementControlsEvent 

New in version 12.0

This event, together with ModifyInlineElementEnabledControlsEvent, serves as a more powerful and flexible replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook']

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent is called after the markup for all enabled controls has been generated. It can be used to either change the markup of a control, to add a new control or to completely remove a control.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent;
use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Utility\GeneralUtility;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-enabled-controls',
    method: 'modifyEnabledControls',
)]
#[AsEventListener(
    identifier: 'my-extension/backend/modify-controls',
    method: 'modifyControls',
)]
final readonly class MyEventListener
{
    public function modifyEnabledControls(ModifyInlineElementEnabledControlsEvent $event): void
    {
        // Enable a control depending on the foreign table
        if ($event->getForeignTable() === 'sys_file_reference' && $event->isControlEnabled('sort')) {
            $event->enableControl('sort');
        }
    }

    public function modifyControls(ModifyInlineElementControlsEvent $event): void
    {
        // Add a custom control depending on the parent table
        if ($event->getElementData()['inlineParentTableName'] === 'tt_content') {
            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
            $event->setControl(
                'tx_my_control',
                '<a href="/some/url" class="btn btn-default t3js-modal-trigger">'
                . $iconFactory->getIcon('my-icon-identifier', IconSize::SMALL)->render()
                . '</a>',
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyInlineElementControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent

Listeners to this Event will be able to modify the controls of an inline element

getControls ( )

Returns all controls with their markup

Returns
array
setControls ( array $controls)

Overwrite the controls

param $controls

the controls

getControl ( string $identifier)

Returns the markup for the requested control

param $identifier

the identifier

Returns
string
setControl ( string $identifier, string $markup)

Set a control with the given identifier and markup IMPORTANT: Overwrites an existing control with the same identifier

param $identifier

the identifier

param $markup

the markup

hasControl ( string $identifier)

Returns whether a control exists for the given identifier

param $identifier

the identifier

Returns
bool
removeControl ( string $identifier)

Removes a control from the inline element, if it exists

param $identifier

the identifier

Return description

Whether the control could be removed

Returns
bool
getElementData ( )

Returns the whole element data

Returns
array
getRecord ( )

Returns the current record of the controls are created for

Returns
array
getParentUid ( )

Returns the uid of the parent (embedding) record (uid or NEW...)

Returns
string
getForeignTable ( )

Returns the table (foreign_table) the controls are created for

Returns
string
getFieldConfiguration ( )

Returns the TCA configuration of the inline record field

Returns
array
isVirtual ( )

Returns whether the current records is only virtually shown and not physically part of the parent record

Returns
bool

ModifyInlineElementEnabledControlsEvent 

New in version 12.0

This event, together with ModifyInlineElementControlsEvent, serves as a more powerful and flexible replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook']

The PSR-14 event \TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent is called before any control markup is generated. It can be used to enable or disable each control. With this event it is therefore possible to enable a control, which is disabled in TCA, only for some use case.

See the ModifyInlineElementControlsEvent example for details.

Example 

API 

class ModifyInlineElementEnabledControlsEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent

Listeners to this Event will be able to modify the state (enabled or disabled) for controls of an inline element

enableControl ( string $identifier)

Enable a control, if it exists

param $identifier

the identifier

Return description

Whether the control could be enabled

Returns
bool
disableControl ( string $identifier)

Disable a control, if it exists

param $identifier

the identifier

Return description

Whether the control could be disabled

Returns
bool
hasControl ( string $identifier)

Returns whether a control exists for the given identifier

param $identifier

the identifier

Returns
bool
isControlEnabled ( string $identifier)

Returns whether the control is enabled.

Note: Will also return FALSE in case no control exists for the requested identifier

param $identifier

the identifier

Returns
bool
getControlsState ( )

Returns all controls with their state (enabled or disabled)

Returns
array
getEnabledControls ( )

Returns all enabled controls

Returns
array
getElementData ( )

Returns the whole element data

Returns
array
getRecord ( )

Returns the current record of the controls are created for

Returns
array
getParentUid ( )

Returns the uid of the parent (embedding) record (uid or NEW...)

Returns
string
getForeignTable ( )

Returns the table (foreign_table) the controls are created for

Returns
string
getFieldConfiguration ( )

Returns the TCA configuration of the inline record field

Returns
array
isVirtual ( )

Returns whether the current records is only virtually shown and not physically part of the parent record

Returns
bool

ModifyLinkExplanationEvent 

New in version 12.0

This event serves as a more powerful and flexible alternative for the removed $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'] hook.

While the removed hook effectively only allowed to modify the link explanation of TCA link fields in case the resolved link type did not already match one of those, implemented by TYPO3 itself, the new event allows to always modify the link explanation of any type. Additionally, this also allows to modify the additionalAttributes, displayed below the actual link explanation field. This is especially useful for extended link handler setups.

To modify the link explanation, the following methods are available:

  • getLinkExplanation(): Returns the current link explanation data
  • setLinkExplanation(): Set the link explanation data
  • getLinkExplanationValue(): Returns a specific link explanation value
  • setLinkExplanationValue(): Sets a specific link explanation value

The link explanation array usually contains the following values:

  • text : The text to show in the link explanation field
  • icon: The markup for the icon, displayed in front of the link explanation field
  • additionalAttributes: The markup for additional attributes, displayed below the link explanation field

The current context can be evaluated using the following methods:

  • getLinkData(): Returns the resolved link data, such as the page uid
  • getLinkParts(): Returns the resolved link parts, such as url, target and additionalParams
  • getElementData(): Returns the full FormEngine $data array for the current element

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-link-explanation',
)]
final readonly class MyEventListener
{
    public function __construct(
        private IconFactory $iconFactory,
    ) {}

    public function __invoke(ModifyLinkExplanationEvent $event): void
    {
        // Use a custom icon for a custom link type
        if ($event->getLinkData()['type'] === 'myCustomLinkType') {
            $event->setLinkExplanationValue(
                'icon',
                $this->iconFactory->getIcon(
                    'my-custom-link-icon',
                    IconSize::SMALL,
                )->render(),
            );
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyLinkExplanationEvent
Fully qualified name
\TYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent

Listeners to this Event will be able to modify the link explanation array, used in FormEngine for link fields

getLinkData ( )
Returns
array
getLinkParts ( )
Returns
array
getElementData ( )
Returns
array
getLinkExplanation ( )
Returns
array
setLinkExplanation ( array $linkExplanation)
param $linkExplanation

the linkExplanation

getLinkExplanationValue ( string $key, ?mixed $default = NULL)
param $key

the key

param $default

the default, default: NULL

Returns
?mixed
setLinkExplanationValue ( string $key, ?mixed $value)
param $key

the key

param $value

the value

ModifyLinkHandlersEvent 

New in version 12.0

This event has been introduced together with ModifyAllowedItemsEvent to serve as a direct replacement for the following removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] . It replaces the method modifyLinkHandlers() in this hook.

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent is triggered before link handlers are executed, allowing listeners to modify the set of handlers that will be used.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/link-handlers',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyLinkHandlersEvent $event): void
    {
        $handler = $event->getLinkHandler('url.');
        $handler['label'] = 'My custom label';
        $event->setLinkHandler('url.', $handler);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyLinkHandlersEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyLinkHandlersEvent

This event allows extensions to modify the list of link handlers and their configuration before they are invoked.

getLinkHandlers ( )
Returns
array<string,array>
getLinkHandler ( string $name)

Gets an individual handler by name.

param $name

The handler name, including trailing period.

Return description

The handler definition, or null if not defined.

Returns
array<string,mixed>|null
setLinkHandler ( string $name, array $handler)

Sets a handler by name, overwriting it if it already exists.

param $name

The handler name, including trailing period.

param $handler

the handler

Returns
$this
getCurrentLinkParts ( )
Returns
array<string,mixed>

ModifyNewContentElementWizardItemsEvent 

New in version 12.0

This event serves as a more powerful and flexible alternative for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'] .

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent is called after TYPO3 has already prepared the wizard items, defined in page TSconfig (mod.wizards.newContentElement.wizardItems).

The event allows listeners to modify any available wizard item as well as adding new ones. It is therefore possible for the listeners to, for example, change the configuration, the position or to remove existing items altogether.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-wizard-items',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyNewContentElementWizardItemsEvent $event): void
    {
        // Add a new wizard item after "textpic"
        $event->setWizardItem(
            'my_element',
            [
                'iconIdentifier' => 'icon-my-element',
                'title' => 'My element',
                'description' => 'My element description',
                'tt_content_defValues' => [
                    'CType' => 'my_element',
                ],
            ],
            ['after' => 'common_textpic'],
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyNewContentElementWizardItemsEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent

Listeners to this Event will be able to modify the wizard items of the new content element wizard component

getWizardItems ( )
Returns
array
setWizardItems ( array $wizardItems)
param $wizardItems

the wizardItems

hasWizardItem ( string $identifier)
param $identifier

the identifier

Returns
bool
getWizardItem ( string $identifier)
param $identifier

the identifier

Returns
?array
setWizardItem ( string $identifier, array $configuration, array $position = [])

Add a new wizard item with configuration at a defined position.

Can also be used to relocate existing items and to modify their configuration.

param $identifier

the identifier

param $configuration

the configuration

param $position

the position, default: []

removeWizardItem ( string $identifier)
param $identifier

the identifier

Returns
bool
getPageInfo ( )

Provides information about the current page making use of the wizard.

Returns
array
getColPos ( )

Provides information about the column position of the button that triggered the wizard.

Returns
?int
getSysLanguage ( )

Provides information about the language used while triggering the wizard.

Returns
int
getUidPid ( )

Provides information about the element to position the new element after (uid) or into (pid).

Returns
int

ModifyPageLayoutContentEvent 

New in version 12.0

This event serves as a replacement for the removed hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawHeaderHook']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawFooterHook']

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent allows to modify page module content.

It is possible to add additional content, overwrite existing content or reorder the content.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-page-module-content',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyPageLayoutContentEvent $event): void
    {
        // Get the current page ID
        $id = (int)($event->getRequest()->getQueryParams()['id'] ?? 0);

        $event->addHeaderContent('Additional header content');

        $event->setFooterContent('Overwrite footer content');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyPageLayoutContentEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent

Listeners to this Event will be able to modify the header and footer content of the page module

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getModuleTemplate ( )
Returns
\TYPO3\CMS\Backend\Template\ModuleTemplate
setHeaderContent ( string $content)

Set content for the header. Can also be used to e.g. reorder existing content.

IMPORTANT: This overwrites existing content from previous listeners!

param $content

the content

addHeaderContent ( string $content)

Add additional content to the header

param $content

the content

getHeaderContent ( )
Returns
string
setFooterContent ( string $content)

Set content for the footer. Can also be used to e.g. reorder existing content.

IMPORTANT: This overwrites existing content from previous listeners!

param $content

the content

addFooterContent ( string $content)

Add additional content to the footer

param $content

the content

getFooterContent ( )
Returns
string

ModifyPageLayoutOnLoginProviderSelectionEvent 

The PSR-14 event \TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent allows to modify variables for the view depending on a special login provider set in the controller.

Example 

API 

class ModifyPageLayoutOnLoginProviderSelectionEvent
Fully qualified name
\TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent

Allows to modify variables for the view depending on a special login provider set in the controller.

getController ( )

Deprecated: Remove in v14.

Returns
\TYPO3\CMS\Backend\Controller\LoginController
getView ( )
Returns
\TYPO3\CMS\Fluid\View\StandaloneView|\TYPO3\CMS\Core\View\ViewInterface
getPageRenderer ( )

Deprecated: Remove in v14.

Returns
\TYPO3\CMS\Core\Page\PageRenderer
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

ModifyQueryForLiveSearchEvent 

New in version 12.0

The PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent can be used to modify the live search queries in the backend.

This can be used to adjust the limit for a specific table or to change the result order.

This event is fired in the \TYPO3\CMS\Backend\Search\LiveSearch\LiveSearch class and allows extensions to modify the query builder instance before execution.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/modify-query-for-live-search-event-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyQueryForLiveSearchEvent $event): void
    {
        // Get the current instance
        $queryBuilder = $event->getQueryBuilder();

        // Change limit depending on the table
        if ($event->getTableName() === 'pages') {
            $queryBuilder->setMaxResults(2);
        }

        // Reset the orderBy part
        $queryBuilder->resetQueryPart('orderBy');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyQueryForLiveSearchEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent

PSR-14 event to modify the query builder instance for the live search

getQueryBuilder ( )
Returns
\TYPO3\CMS\Core\Database\Query\QueryBuilder
getTableName ( )
Returns
string

ModifyRecordListHeaderColumnsEvent 

New in version 11.4

Changed in version 12.0

Due to the integration of EXT:recordlist into EXT:backend the namespace of the event changed from \TYPO3\CMS\Recordlist\Event\ModifyRecordListHeaderColumnsEvent to \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent . For TYPO3 v12 the moved class is available as an alias under the old namespace to allow extensions to be compatible with TYPO3 v11 and v12.

The PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent allows to modify the header columns for a table in the record list.

Usage 

See combined usage example.

API 

class ModifyRecordListHeaderColumnsEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent

An event to modify the header columns for a table in the RecordList

setColumn ( string $column, string $columnName = '')

Add a new column or override an existing one. Latter is only possible, in case $columnName is given. Otherwise, the column will be added with a numeric index, which is generally not recommended.

Note: Due to the behaviour of DatabaseRecordList, just adding a column does not mean that it is also displayed. The internal $fieldArray needs to be adjusted as well. This method only adds the column to the data array. Therefore, this method should mainly be used to edit existing columns, e.g. change their label.

param $column

the column

param $columnName

the columnName, default: ''

hasColumn ( string $columnName)

Whether the column exists

param $columnName

the columnName

Returns
bool
getColumn ( string $columnName)

Get column by its name

param $columnName

the columnName

Return description

The column or NULL if the column does not exist

Returns
string|null
removeColumn ( string $columnName)

Remove column by its name

param $columnName

the columnName

Return description

Whether the column could be removed - Will thereforereturn FALSE if the column to remove does not exist.

Returns
bool
setColumns ( array $columns)
param $columns

the columns

getColumns ( )
Returns
array
setHeaderAttributes ( array $headerAttributes)
param $headerAttributes

the headerAttributes

getHeaderAttributes ( )
Returns
array
getTable ( )
Returns
string
getRecordIds ( )
Returns
array
getRecordList ( )

Returns the current DatabaseRecordList instance.

Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyRecordListRecordActionsEvent 

New in version 11.4

Changed in version 12.0

Due to the integration of EXT:recordlist into EXT:backend the namespace of the event changed from \TYPO3\CMS\Recordlist\Event\ModifyRecordListRecordActionsEvent to \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent . For TYPO3 v12 the moved class is available as an alias under the old namespace to allow extensions to be compatible with TYPO3 v11 and v12.

The PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent allows to modify the displayed record actions (for example edit, copy, delete) for a table in the record list.

Usage 

See combined usage example.

API 

class ModifyRecordListRecordActionsEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent

An event to modify the displayed record actions (e.g.

"edit", "copy", "delete") for a table in the RecordList.

setAction ( string $action, string $actionName = '', string $group = '', string $before = '', string $after = '')

Add a new action or override an existing one. Latter is only possible, in case $columnName is given. Otherwise, the column will be added with a numeric index, which is generally not recommended. It's also possible to define the position of an action with either the "before" or "after" argument, while their value must be an existing action.

Note: In case non or an invalid $group is provided, the new action will be added to the secondary group.

param $action

the action

param $actionName

the actionName, default: ''

param $group

the group, default: ''

param $before

the before, default: ''

param $after

the after, default: ''

hasAction ( string $actionName, string $group = '')

Whether the action exists in the given group. In case non or an invalid $group is provided, both groups will be checked.

param $actionName

the actionName

param $group

the group, default: ''

Returns
bool
getAction ( string $actionName, string $group = '')

Get action by its name. In case the action exists in both groups and non or an invalid $group is provided, the action from the "primary" group will be returned.

param $actionName

the actionName

param $group

the group, default: ''

Returns
?string
removeAction ( string $actionName, string $group = '')

Remove action by its name. In case the action exists in both groups and non or an invalid $group is provided, the action will be removed from both groups.

param $actionName

the actionName

param $group

the group, default: ''

Return description

Whether the action could be removed - Will thereforereturn FALSE if the action to remove does not exist.

Returns
bool
getActionGroup ( string $group)

Get the actions of a specific group

param $group

the group

Returns
?array
setActions ( array $actions)
param $actions

the actions

getActions ( )
Returns
array
getTable ( )
Returns
string
getRecord ( )
Returns
array
getRecordList ( )

Returns the current DatabaseRecordList instance.

Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyRecordListTableActionsEvent 

New in version 11.4

Changed in version 12.0

Due to the integration of EXT:recordlist into EXT:backend the namespace of the event changed from \TYPO3\CMS\Recordlist\Event\ModifyRecordListTableActionsEvent to \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent . For TYPO3 v12 the moved class is available as an alias under the old namespace to allow extensions to be compatible with TYPO3 v11 and v12.

The PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent allows to modify the multi record selection actions (for example edit, copy to clipboard) for a table in the record list.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyRecordActions',
)]
#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyHeaderColumns',
)]
#[AsEventListener(
    identifier: 'my-extension/recordlist/my-event-listener',
    method: 'modifyTableActions',
)]
final readonly class MyEventListener
{
    public function __construct(
        private LoggerInterface $logger,
    ) {}

    public function modifyRecordActions(ModifyRecordListRecordActionsEvent $event): void
    {
        $currentTable = $event->getTable();

        // Add a custom action for a custom table in the secondary action bar, before the "move" action
        if ($currentTable === 'my_custom_table' && !$event->hasAction('myAction')) {
            $event->setAction(
                '<button>My Action</button>',
                'myAction',
                'secondary',
                'move',
            );
        }

        // Remove the "viewBig" action in case more than 4 actions exist in the group
        if (count($event->getActionGroup('secondary')) > 4 && $event->hasAction('viewBig')) {
            $event->removeAction('viewBig');
        }

        // Move the "delete" action after the "edit" action
        $event->setAction('', 'delete', 'primary', '', 'edit');
    }

    public function modifyHeaderColumns(ModifyRecordListHeaderColumnsEvent $event): void
    {
        // Change label of "control" column
        $event->setColumn('Custom Controls', '_CONTROL_');

        // Add a custom class for the table header row
        $event->setHeaderAttributes(['class' => 'my-custom-class']);
    }

    public function modifyTableActions(ModifyRecordListTableActionsEvent $event): void
    {
        // Remove "edit" action and log, if this failed
        $actionRemoved = $event->removeAction('unknown');
        if (!$actionRemoved) {
            $this->logger->warning('Action "unknown" could not be removed');
        }

        // Add a custom clipboard action after "copyMarked"
        $event->setAction('<button>My action</button>', 'myAction', '', 'copyMarked');

        // Set a custom label for the case, no actions are available for the user
        $event->setNoActionLabel('No actions available due to missing permissions.');
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyRecordListTableActionsEvent
Fully qualified name
\TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent

An event to modify the multi record selection actions (e.g.

"edit", "copy to clipboard") for a table in the RecordList.

setAction ( string $action, string $actionName = '', string $before = '', string $after = '')

Add a new action or override an existing one. Latter is only possible, in case $actionName is given. Otherwise, the action will be added with a numeric index, which is generally not recommended. It's also possible to define the position of an action with either the "before" or "after" argument, while their value must be an existing action.

param $action

the action

param $actionName

the actionName, default: ''

param $before

the before, default: ''

param $after

the after, default: ''

hasAction ( string $actionName)

Whether the action exists

param $actionName

the actionName

Returns
bool
getAction ( string $actionName)

Get action by its name

param $actionName

the actionName

Return description

The action or NULL if the action does not exist

Returns
string|null
removeAction ( string $actionName)

Remove action by its name

param $actionName

the actionName

Return description

Whether the action could be removed - Will thereforereturn FALSE if the action to remove does not exist.

Returns
bool
setActions ( array $actions)
param $actions

the actions

getActions ( )
Returns
array
setNoActionLabel ( string $noActionLabel)
param $noActionLabel

the noActionLabel

getNoActionLabel ( )

Get the label, which will be displayed, in case no action is available for the current user. Note: If this returns an empty string, this only means that no other listener set a label before. TYPO3 will always fall back to a default if this remains empty.

Returns
string
getTable ( )
Returns
string
getRecordIds ( )
Returns
array
getRecordList ( )

Returns the current DatabaseRecordList instance.

Returns
\TYPO3\CMS\Backend\RecordList\DatabaseRecordList

ModifyResultItemInLiveSearchEvent 

New in version 12.2

The PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent allows extension developers to take control over search result items rendered in the backend search.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent;
use TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider;
use TYPO3\CMS\Backend\Search\LiveSearch\ResultItemAction;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

#[AsEventListener(
    identifier: 'my-extension/add-live-search-result-actions-listener',
)]
final readonly class MyEventListener
{
    private LanguageService $languageService;

    public function __construct(
        private IconFactory $iconFactory,
        LanguageServiceFactory $languageServiceFactory,
        private UriBuilder $uriBuilder,
    ) {
        $this->languageService = $languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
    }

    public function __invoke(ModifyResultItemInLiveSearchEvent $event): void
    {
        $resultItem = $event->getResultItem();
        if ($resultItem->getProviderClassName() !== DatabaseRecordProvider::class) {
            return;
        }

        if (($resultItem->getExtraData()['table'] ?? null) === 'tt_content') {
            /**
             * WARNING: THIS EXAMPLE OMITS ANY ACCESS CHECK FOR SIMPLICITY REASONS - DO NOT USE AS-IS
             */
            $showHistoryAction = (new ResultItemAction('view_history'))
                ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:history'))
                ->setIcon($this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL))
                ->setUrl((string)$this->uriBuilder->buildUriFromRoute('record_history', [
                    'element' => $resultItem->getExtraData()['table'] . ':' . $resultItem->getExtraData()['uid'],
                ]));
            $resultItem->addAction($showHistoryAction);
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class ModifyResultItemInLiveSearchEvent
Fully qualified name
\TYPO3\CMS\Backend\Search\Event\ModifyResultItemInLiveSearchEvent

PSR-14 event to modify as result item created by the live search

getResultItem ( )
Returns
\TYPO3\CMS\Backend\Search\LiveSearch\ResultItem

PageContentPreviewRenderingEvent 

New in version 12.0

This event has been introduced which serves as a drop-in replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] hook.

Use the PSR-14 event \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent to ship an alternative rendering for a specific content type or to manipulate the record data of a content element.

Example 

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/preview-rendering-example-ctype',
)]
final readonly class MyEventListener
{
    public function __invoke(PageContentPreviewRenderingEvent $event): void
    {
        if ($event->getTable() !== 'tt_content') {
            return;
        }

        if ($event->getRecordType() === 'example_ctype') {
            $event->setPreviewContent('<div>...</div>');
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class PageContentPreviewRenderingEvent
Fully qualified name
\TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent

Use this Event to have a custom preview for a content type in the Page Module

getTable ( )
Returns
string
getRecordType ( )
Returns
string
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

getPageLayoutContext ( )
Returns
\TYPO3\CMS\Backend\View\PageLayoutContext
getPreviewContent ( )
Returns
?string
setPreviewContent ( string $content)
param $content

the content

isPropagationStopped ( )
Returns
bool

RenderAdditionalContentToRecordListEvent 

New in version 11.0

This event supersedes the hooks

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['recordlist/Modules/Recordlist/index.php']['drawHeaderHook']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['recordlist/Modules/Recordlist/index.php']['drawFooterHook']

The hooks are removed in TYPO3 v12.

Changed in version 12.0

Due to the integration of EXT:recordlist into EXT:backend the namespace of the event changed from \TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent to \TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent . For TYPO3 v12 the moved class is available as an alias under the old namespace to allow extensions to be compatible with TYPO3 v11 and v12.

The PSR-14 event \TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent allows to add content before or after the main content of the List module.

Example 

API 

class RenderAdditionalContentToRecordListEvent
Fully qualified name
\TYPO3\CMS\Backend\Controller\Event\RenderAdditionalContentToRecordListEvent

Add content above or below the main content of the record list

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
addContentAbove ( string $contentAbove)
param $contentAbove

the contentAbove

addContentBelow ( string $contentBelow)
param $contentBelow

the contentBelow

getAdditionalContentAbove ( )
Returns
string
getAdditionalContentBelow ( )
Returns
string

SudoModeRequiredEvent 

New in version 12.4.32 / 13.4.13

This event was introduced by security advisory TYPO3-CORE-SA-2025-013 to address challenges with single sign-on (SSO) providers.

The PSR-14 event \TYPO3\CMS\Backend\Backend\Event\SudoModeRequiredEvent is triggered before showing the sudo-mode verification dialog when managing backend user accounts.

This step-up authentication, introduced as part of the fix for TYPO3-CORE-SA-2025-013, helps prevent unauthorized password changes. However, it may pose challenges when using remote single sign-on (SSO) systems, which typically do not support a separate step-up verification process.

This event allows developers to skip / bypass the step-up authentication process and uses custom logic, such as identifying users authenticated through an SSO system.

Example: Use an event listener to skip step-up authentication for SSO users 

The following example demonstrates how to use an event listener to skip step-up authentication for be_users records that have an active is_sso flag:

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

  Vendor\MyExtension\EventListener\SkipSudoModeDialog:
    tags:
      - name: event.listener
        identifier: 'ext-myextension/skip-sudo-mode-dialog'
  Vendor\MyExtension\EventListener\StaticPasswordVerification:
    tags:
      - name: event.listener
        identifier: 'ext-myextension/static-password-verification'
Copied!
EXT:my_extension/Classes/EventListener/SkipSudoModeDialog.php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Hooks\DataHandlerAuthenticationContext;
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessSubjectInterface;
use TYPO3\CMS\Backend\Security\SudoMode\Access\TableAccessSubject;
use TYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeRequiredEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

final class SkipSudoModeDialog
{
    public function __invoke(SudoModeRequiredEvent $event): void
    {
        // Ensure the event context matches DataHandler operations
        if ($event->getClaim()->origin !== DataHandlerAuthenticationContext::class) {
            return;
        }

        // Filter for TableAccessSubject types only
        $tableAccessSubjects = array_filter(
            $event->getClaim()->subjects,
            static fn(AccessSubjectInterface $subject): bool => $subject instanceof TableAccessSubject,
        );

        // Abort if there are unhandled subject types
        if ($event->getClaim()->subjects !== $tableAccessSubjects) {
            return;
        }

        /** @var list<TableAccessSubject> $tableAccessSubjects */
        foreach ($tableAccessSubjects as $subject) {
            // Expecting format: tableName.fieldName.id
            if (substr_count($subject->getSubject(), '.') !== 2) {
                return;
            }

            [$tableName, $fieldName, $id] = explode('.', $subject->getSubject());

            // Only handle be_users table
            if ($tableName !== 'be_users') {
                return;
            }

            // Skip if ID is not a valid integer (e.g., 'NEW' records)
            if (!MathUtility::canBeInterpretedAsInteger($id)) {
                continue;
            }

            $record = BackendUtility::getRecord($tableName, $id);

            // Abort if any record does not use SSO
            if (empty($record['is_sso'])) {
                return;
            }
        }

        // All conditions met — disable verification
        $event->setVerificationRequired(false);
    }
}
Copied!

See also: StaticPasswordVerification example

API 

class SudoModeRequiredEvent
Fully qualified name
\TYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeRequiredEvent
getClaim ( )
Returns
\TYPO3\CMS\Backend\Security\SudoMode\Access\AccessClaim
isVerificationRequired ( )
Returns
bool
setVerificationRequired ( bool $verificationRequired)
param $verificationRequired

the verificationRequired

SudoModeVerifyEvent 

New in version 12.4.32 / 13.4.13

This event was introduced by the fix for security advisory TYPO3-CORE-SA-2025-013 to address challenges with single sign-on (SSO) providers.

The PSR-14 event \TYPO3\CMS\Backend\Backend\Event\SudoModeVerifyEvent is triggered before a password submitted in sudo-mode verification dialog is verified for backend user accounts.

This step-up authentication mechanism, introduced as part of the fix for TYPO3-CORE-SA-2025-013, may pose challenges when using remote single sign-on (SSO) systems because they do not support a dedicated verification step.

This event allows developers to change the verification logic of step-up authentication, by conditionally allowing or denying verification based on custom logic — for example, by identifying users authenticated via an SSO system.

Example: Use an event listener to modify the verification of password in sudo mode 

The following demonstrates using the event to statically check the password for an expected hash.

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

  Vendor\MyExtension\EventListener\SkipSudoModeDialog:
    tags:
      - name: event.listener
        identifier: 'ext-myextension/skip-sudo-mode-dialog'
  Vendor\MyExtension\EventListener\StaticPasswordVerification:
    tags:
      - name: event.listener
        identifier: 'ext-myextension/static-password-verification'
Copied!
EXT:my_extension/Classes/EventListener/StaticPasswordVerification.php
<?php

declare(strict_types=1);

namespace Example\Demo\EventListener;

use TYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeVerifyEvent;

final class StaticPasswordVerification
{
    public function __invoke(SudoModeVerifyEvent $event): void
    {
        $calculatedHash = hash('sha256', $event->getPassword());
        // static hash of `dontdothis` - just used as proof-of-concept
        // side-note: in production, make use of strong salted password
        $expectedHash = '3382f2e21a5471b52a85bc32ab59ab2c467f6e3cb112aef295323874f423994c';

        if (hash_equals($expectedHash, $calculatedHash)) {
            $event->setVerified(true);
        }
    }
}
Copied!

See also: SkipSudoModeDialog example.

API 

class SudoModeVerifyEvent
Fully qualified name
\TYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeVerifyEvent
getClaim ( )
Returns
\TYPO3\CMS\Backend\Security\SudoMode\Access\AccessClaim
getPassword ( )
Returns
string
isUseInstallToolPassword ( )
Returns
bool
isVerified ( )
Returns
bool
setVerified ( bool $verified)
param $verified

the verified

SwitchUserEvent 

The PSR-14 event \TYPO3\CMS\Backend\Authentication\Event\SwitchUserEvent is dispatched when a "SU" (switch user) action has been triggered.

Example 

API 

class SwitchUserEvent
Fully qualified name
\TYPO3\CMS\Backend\Authentication\Event\SwitchUserEvent

This event is triggered when a "SU" (switch user) action has been triggered

getSessionId ( )
Returns
string
getTargetUser ( )
Returns
array
getCurrentUser ( )
Returns
array

SystemInformationToolbarCollectorEvent 

The PSR-14 event \TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent allows to enrich the system information toolbar in the TYPO3 backend top toolbar with various information.

Example: Display release information in "System Information" toolbar window 

The event SystemInformationToolbarCollectorEvent gets you the object SystemInformationToolbarItem on which you can call method addSystemInformation() to add system information items or method addSystemMessage() to add messages.

EXT:my_extension/Classes/EventListener/AddReleaseInfoToSystemInformationEventListener.php
<?php

namespace MyVendor\MyExample\EventListener;

use TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent;
use TYPO3\CMS\Backend\Toolbar\InformationStatus;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'my-extension/backend/add-release-info-to-system-information',
)]
final class AddReleaseInfoToSystemInformationEventListener
{
    public function __invoke(SystemInformationToolbarCollectorEvent $event): void
    {
        [$releaseDate, $releaseHash, $releaseAge] = $this->getReleaseData();

        $event->getToolbarItem()->addSystemInformation(
            'Release nr / hash',
            $releaseHash ?? 'n/a',
            'actions-cloud-upload',
        );

        if ($releaseAge > 14) {
            $event->getToolbarItem()->addSystemMessage(
                sprintf('Release is %d days old', $releaseAge),
                InformationStatus::WARNING,
                1,
            );
        }

        $event->getToolbarItem()->addSystemInformation(
            'Release date',
            $releaseDate ?? 'n/a',
            'actions-calendar',
        );
    }

    private function getReleaseData(): array
    {
        // Todo:: Implement
        return ['01.10.2025 11:11:11', 'abc123', 15];
    }
}
Copied!

The messages will then be displayed like this:

TYPO3 Backend Screenshot with custom System Information Icons and a custom message

The release information is now displayed in the "System Information" toolbar item

API 

class SystemInformationToolbarCollectorEvent
Fully qualified name
\TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent

An event to enrich the system information toolbar in the TYPO3 Backend top toolbar with various information

getToolbarItem ( )
Returns
\TYPO3\CMS\Backend\Backend\ToolbarItems\SystemInformationToolbarItem

Core 

The following list contains PSR-14 events in EXT:core .

Contents:

AfterGroupsResolvedEvent 

When user groups are loaded, for example when a backend editor's groups and permissions are calculated, a new PSR-14 event AfterGroupsResolvedEvent is fired.

This event contains a list of retrieved groups from the database which can be modified via event listeners. For example, more groups might be added when a particular user logs in or is seated at a special location.

Example 

API 

class AfterGroupsResolvedEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterGroupsResolvedEvent

Event fired after user groups have been resolved for a specific user

getSourceDatabaseTable ( )
Return description

'be_groups' or 'fe_groups' depending on context.

Returns
string
getGroups ( )

List of group records including sub groups as resolved by core.

Note order is important: A user with main groups "1,2", where 1 has sub group 3, results in "3,1,2" as record list array - sub groups are listed before the group that includes the sub group.

Returns
array
setGroups ( array $groups)

List of group records as manipulated by the event.

param $groups

the groups

getOriginalGroupIds ( )

List of group uids directly attached to the user

Returns
array
getUserData ( )

Full user record with all fields

Returns
array

AfterUserLoggedInEvent 

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'] .

New in version 13.0

The event is also dispatched, when a successful frontend user login is performed.

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent is to trigger any kind of action when a backend or frontend user has been successfully logged in.

The TYPO3 Core itself uses this event in the TYPO3 backend to send an email to a user, if the user has successfully logged in. See EXT:backend/Classes/Security/EmailLoginNotification.php (GitHub).

Example 

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;

#[AsEventListener(
    identifier: 'my-extension/after-user-logged-in',
)]
final readonly class MyEventListener
{
    public function __invoke(AfterUserLoggedInEvent $event): void
    {
        if (
            $event->getUser() instanceof BackendUserAuthentication
            && $event->getUser()->isAdmin()
        ) {
            // Do something like: Clear all caches after login
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterUserLoggedInEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent

Event fired after a user has been actively logged in to backend (incl. possible MFA) or frontend.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getRequest ( )
Returns
?\Psr\Http\Message\ServerRequestInterface

AfterUserLoggedOutEvent 

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent is to trigger any kind of action when a user has been successfully logged out.

Example 

API 

class AfterUserLoggedOutEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedOutEvent

Event fired after a user has been actively logged out.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication

BeforeRequestTokenProcessedEvent 

New in version 12.1

The event \TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent allows to intercept or adjust a request token during active user authentication process.

Example 

The event can be used to generate the request token individually. This can be the case when you are not using a login callback and have not the possibility to submit a request token:

EXT:my_extension/Classes/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent;
use TYPO3\CMS\Core\Security\RequestToken;

#[AsEventListener(
    identifier: 'my-extension/process-request-token-listener',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeRequestTokenProcessedEvent $event): void
    {
        $user = $event->getUser();
        $requestToken = $event->getRequestToken();
        // fine, there is a valid request token
        if ($requestToken instanceof RequestToken) {
            return;
        }

        // Validate individual requirements/checks
        // ...
        $event->setRequestToken(
            RequestToken::create('core/user-auth/' . strtolower($user->loginType)),
        );
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BeforeRequestTokenProcessedEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent

Event fired before request-token is processed.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getRequestToken ( )
Returns
\TYPO3\CMS\Core\Security\RequestToken|false|?null
setRequestToken ( ?TYPO3\CMS\Core\Security\RequestToken|false|null $requestToken)
param $requestToken

the requestToken

BeforeUserLogoutEvent 

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent is to trigger any kind of action before a user will be logged out.

The event has the possibility to bypass the regular logout process by TYPO3 (removing the cookie and the user session) by calling $event->disableRegularLogoutProcess() in an event listener.

Example 

API 

class BeforeUserLogoutEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\BeforeUserLogoutEvent

Event fired before a user is going to be actively logged out.

An option to interrupt the regular logout flow from TYPO3 Core (so you can do this yourself) is also available.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
disableRegularLogoutProcess ( )
enableRegularLogoutProcess ( )
shouldLogout ( )
Returns
bool
getUserSession ( )
Returns
?\TYPO3\CMS\Core\Session\UserSession

LoginAttemptFailedEvent 

New in version 12.3

The event replaces the deprecated hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent is to allow to notify remote systems about failed logins.

Example 

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent;

#[AsEventListener(
    identifier: 'my-extension/login-attempt-failed',
)]
final readonly class MyEventListener
{
    public function __invoke(LoginAttemptFailedEvent $event): void
    {
        $normalizedParams = $event->getRequest()->getAttribute('normalizedParams');
        if ($normalizedParams->getRemoteAddress() !== '198.51.100.42') {
            // send an email because an external user attempt failed
        }
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class LoginAttemptFailedEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent

Event fired after a login attempt failed.

getUser ( )
Returns
\TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
getLoginData ( )
Returns
array
isFrontendAttempt ( )
Returns
bool
isBackendAttempt ( )
Returns
bool
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

CacheFlushEvent 

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheFlushEvent is fired when caches are to be cleared.

Example 

API 

class CacheFlushEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheFlushEvent

Event fired when caches are to be cleared

getGroups ( )
Returns
array
hasGroup ( string $group)
param $group

the group

Returns
bool
getErrors ( )
Returns
array
addError ( string $error)
param $error

the error

CacheWarmupEvent 

The PSR-14 event \TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent is fired when caches are to be warmed up.

Example 

API 

class CacheWarmupEvent
Fully qualified name
\TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent

Event fired when caches are to be warmed up

getGroups ( )
Returns
array
hasGroup ( string $group)
param $group

the group

Returns
bool
getErrors ( )
Returns
array
addError ( string $error)
param $error

the error

AfterFlexFormDataStructureIdentifierInitializedEvent 

New in version 12.0

This event was introduced to replace and improve the method parseDataStructureByIdentifierPostProcess() of the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example 

EXT:my_extension/Classes/Configuration/EventListener/FlexFormParsingModifyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent;

#[AsEventListener(
    identifier: 'my-extension/set-data-structure',
    method: 'setDataStructure',
)]
#[AsEventListener(
    identifier: 'my-extension/modify-data-structure',
    method: 'modifyDataStructure',
)]
#[AsEventListener(
    identifier: 'my-extension/set-data-structure-identifier',
    method: 'setDataStructureIdentifier',
)]
#[AsEventListener(
    identifier: 'my-extension/modify-data-structure-identifier',
    method: 'modifyDataStructureIdentifier',
)]
final readonly class FlexFormParsingModifyEventListener
{
    public function setDataStructure(BeforeFlexFormDataStructureParsedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'my_custom_type') {
            $event->setDataStructure('FILE:EXT:my_extension/Configuration/FlexForms/MyFlexform.xml');
        }
    }

    public function modifyDataStructure(AfterFlexFormDataStructureParsedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'my_custom_type') {
            $parsedDataStructure = $event->getDataStructure();
            $parsedDataStructure['sheets']['sDEF']['ROOT']['sheetTitle'] = 'Some dynamic custom sheet title';
            $event->setDataStructure($parsedDataStructure);
        }
    }

    public function setDataStructureIdentifier(BeforeFlexFormDataStructureIdentifierInitializedEvent $event): void
    {
        if ($event->getTableName() === 'tx_myextension_domain_model_sometable') {
            $event->setIdentifier([
                'type' => 'my_custom_type',
            ]);
        }
    }

    public function modifyDataStructureIdentifier(AfterFlexFormDataStructureIdentifierInitializedEvent $event): void
    {
        $identifier = $event->getIdentifier();
        if (($identifier['type'] ?? '') === 'some_other_type') {
            $identifier['type'] = 'my_custom_type';
        }
        $event->setIdentifier($identifier);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class AfterFlexFormDataStructureIdentifierInitializedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent

Listeners to this event are able to modify or enhance the data structure identifier, which is used for a given TCA flex field.

This event can be used to add additional data to an identifier. Be careful here, especially if stuff from the source record like uid or pid is added! This may easily lead to issues with data handler details like copy or move records, localization and version overlays. Test this very well! Multiple listeners may add information to the same identifier here - take care to namespace array keys. Information added here can be later used in the data structure related PSR-14 Events (BeforeFlexFormDataStructureParsedEvent and AfterFlexFormDataStructureParsedEvent) again.

See the note on FlexFormTools regarding the schema of $dataStructure.

getFieldTca ( )

Returns the full TCA of the currently handled field, having type=flex set.

Returns
array
getTableName ( )
Returns
string
getFieldName ( )
Returns
string
getRow ( )

Returns the whole database row of the current record.

Returns
array
setIdentifier ( array $identifier)

Allows to modify or completely replace the initialized data structure identifier.

param $identifier

the identifier

getIdentifier ( )

Returns the initialized data structure identifier, which has either been defined by an event listener or set to the default by the FlexFormTools component.

Returns
array

AfterFlexFormDataStructureParsedEvent 

New in version 12.0

This event was introduced to replace and improve the method getDataStructureIdentifierPostProcess() of the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example 

Have a look at the combined example.

API 

class AfterFlexFormDataStructureParsedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent

Listeners to this event are able to modify or enhance a flex form data structure that corresponds to a given identifier, after it was parsed and before it is used by further components.

Note: Since this event is not stoppable, all registered listeners are called. Therefore, you might want to namespace your identifiers in a way, that there is little chance they overlap (e.g. prefix with extension name).

See the note on FlexFormTools regarding the schema of $dataStructure.

getIdentifier ( )
Returns
array
getDataStructure ( )

Returns the current data structure, which has been processed and parsed by the FlexFormTools component. Might contain additional data from previously called listeners.

Returns
array
setDataStructure ( array $dataStructure)

Allows to modify or completely replace the parsed data structure identifier.

param $dataStructure

the dataStructure

AfterTcaCompilationEvent 

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent is dispatched after $GLOBALS['TCA'] is built to allow to further manipulate the TCA.

Example 

API 

class AfterTcaCompilationEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent

Event after $tca which later becomes $GLOBALS['TCA'] has been built.

Allows to further manipulate $tca before it is cached and set as $GLOBALS['TCA'].

getTca ( )
Returns
array
setTca ( array $tca)
param $tca

the tca

BeforeFlexFormDataStructureIdentifierInitializedEvent 

New in version 12.0

This event was introduced to replace and improve the method getDataStructureIdentifierPreProcess() of the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example 

Have a look at the combined example.

API 

class BeforeFlexFormDataStructureIdentifierInitializedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent

Listeners to this event are able to specify the data structure identifier, used for a given TCA flex field.

Listeners should call ->setIdentifier() to set the identifier or ignore the event to allow other listeners to set it. Do not set an empty string as this will immediately stop event propagation!

The identifier SHOULD include the keys specified in the Identifier definition on FlexFormTools, and nothing else. Adding other keys may or may not work, depending on other code that is enabled, and they are not guaranteed nor covered by BC guarantees.

Warning: If adding source record details like the uid or pid here, this may turn out to be fragile. Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may break in between different core versions. It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see the core internal 'tca' and 'record' return values below

See the note on FlexFormTools regarding the schema of $dataStructure.

getFieldTca ( )

Returns the full TCA of the currently handled field, having type=flex set.

Returns
array
getTableName ( )
Returns
string
getFieldName ( )
Returns
string
getRow ( )

Returns the whole database row of the current record.

Returns
array
setIdentifier ( array $identifier)

Allows to define the data structure identifier for the TCA field.

Setting an identifier will immediately stop propagation. Avoid setting this parameter to an empty array as this will also stop propagation.

param $identifier

the identifier

getIdentifier ( )

Returns the current data structure identifier, which will always be null for listeners, since the event propagation is stopped as soon as a listener defines an identifier.

Returns
?array
isPropagationStopped ( )
Returns
bool

Migration 

Using the removed hook method getDataStructureIdentifierPreProcess() of the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] previously required implementations to always return an array.

This means, implementations returned an empty array in case they did not want to set an identifier, allowing further implementations to be called.

This behaviour has now changed. As soon as a listener sets the identifier using the setIdentifier() method, the event propagation is stopped immediately and no further listeners are being called. Therefore, listeners should avoid setting an empty array but should just "return" without any change to the $event object in such a case.

BeforeFlexFormDataStructureParsedEvent 

New in version 12.0

This event was introduced to replace and improve the method parseDataStructureByIdentifierPreProcess() of the hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] .

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent can be used to control the FlexForm parsing in an object-oriented approach.

Example 

Have a look at the combined example.

API 

class BeforeFlexFormDataStructureParsedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent

Listeners to this event are able to specify a flex form data structure that corresponds to a given identifier.

Listeners should call ->setDataStructure() to set the data structure (this can either be a resolved data structure string, a "FILE:" reference or a fully parsed data structure as array) or ignore the event to allow other listeners to set it. Do not set an empty array or string as this will immediately stop event propagation!

See the note on FlexFormTools regarding the schema of $dataStructure.

getDataStructure ( )

Returns the current data structure, which will always be null for listeners, since the event propagation is stopped as soon as a listener sets a data structure.

Returns
array|string|?null
setDataStructure ( array|string $dataStructure)

Allows to either set an already parsed data structure as array, a file reference or the XML structure as string. Setting a data structure will immediately stop propagation. Avoid setting this parameter to an empty array or string as this will also stop propagation.

param $dataStructure

the dataStructure

getIdentifier ( )
Returns
array
isPropagationStopped ( )
Returns
bool

Migration 

Using the removed hook method parseDataStructureByIdentifierPreProcess() of hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] previously required implementations to always return an array or string. Implementations returned an empty array or empty string in case they did not want to set a data structure.

This behaviour has now changed. As soon as a listener sets a data structure using the setDataStructure() method, the event propagation is stopped immediately and no further listeners are called.

Therefore, listeners should avoid setting an empty array or an empty string but should just "return" without any change to the $event object in such a case.

BeforeTcaOverridesEvent 

New in version 13.0

A PSR-14 event \TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent enables developers to listen to the state between loaded base TCA and merging of TCA overrides.

It can be used to dynamically generate TCA and add it as additional base TCA. This is especially useful for "TCA generator" extensions, which add TCA based on another resource, while still enabling users to override TCA via the known TCA overrides API.

Example 

EXT:my_extension/Configuration/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

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

#[AsEventListener(
    identifier: 'my-extension/before-tca-overrides',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeTcaOverridesEvent $event): void
    {
        $tca = $event->getTca();
        $tca['tt_content']['columns']['header']['config']['max'] = 100;
        $event->setTca($tca);
    }
}
Copied!

New in version 13.0

API 

class BeforeTcaOverridesEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent

Event before $tca which later becomes $GLOBALS['TCA'] is overridden by TCA/Overrides.

Allows to manipulate $tca, before overrides are merged.

getTca ( )
Returns
array
setTca ( array $tca)
param $tca

the tca

ModifyLoadedPageTsConfigEvent 

Extensions can modify page TSconfig entries that can be overridden or added, based on the root line.

Changed in version 12.2

The event has moved its namespace from \TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent to \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent . Apart from that no changes were made. TYPO3 v12 triggers both the old and the new event, and TYPO3 v13 stopped calling the old event.

Example 

EXT:my_extension/Classes/Configuration/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;

#[AsEventListener(
    identifier: 'my-extension/configuration/loader',
)]
final readonly class MyEventListener
{
    public function __invoke(ModifyLoadedPageTsConfigEvent $event): void
    {
        // ... your logic
    }
}
Copied!

API 

class ModifyLoadedPageTsConfigEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent

Extensions can modify page TSconfig entries that can be overridden or added, based on the root line

getTsConfig ( )
Returns
array
addTsConfig ( string $tsConfig)
param $tsConfig

the tsConfig

setTsConfig ( array $tsConfig)
param $tsConfig

the tsConfig

getRootLine ( )
Returns
array

SiteConfigurationBeforeWriteEvent 

New in version 12.0

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent allows the modification of the site configuration array before writing the configuration to disk.

Example 

API 

class SiteConfigurationBeforeWriteEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent

Event fired before a site configuration is written to a yaml file allows dynamic modification of the site's configuration before writing.

getSiteIdentifier ( )
Returns
string
getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

overwrite the configuration array of the site

SiteConfigurationLoadedEvent 

New in version 12.0

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent allows the modification of the site configuration array before loading the configuration.

Example 

The example adds a route enhancer configuration provided by an extension with an event listener automatically. This way, there is no need to add an import manually to the site configuration.

EXT:my_extension/Classes/Configuration/EventListener/ImportRoutesIntoSiteConfiguration.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Utility\ArrayUtility;

#[AsEventListener(
    identifier: 'my-extension/import-routes-into-site-configuration',
)]
final readonly class ImportRoutesIntoSiteConfiguration
{
    private const ROUTES = 'EXT:my_extension/Configuration/Routes/Routes.yaml';

    public function __construct(
        private YamlFileLoader $yamlFileLoader,
    ) {}

    public function __invoke(SiteConfigurationLoadedEvent $event): void
    {
        $routeConfiguration = $this->yamlFileLoader->load(self::ROUTES);
        $siteConfiguration = $event->getConfiguration();

        // Using this method instead of array_merge_recursive will
        // prevent duplicate keys, and also allow to use the special
        // "_UNSET" handling properly.
        ArrayUtility::mergeRecursiveWithOverrule(
            $siteConfiguration,
            $routeConfiguration,
        );

        $event->setConfiguration($siteConfiguration);
    }
}
Copied!

For more sophisticated examples, see also Automatically register route enhancer definitions stored in TYPO3 extensions.

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class SiteConfigurationLoadedEvent
Fully qualified name
\TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent

Event after a site configuration has been read from a yaml file before it is cached - allows dynamic modification of the site's configuration.

getSiteIdentifier ( )
Returns
string
getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

overwrite the configuration array of the site

BootCompletedEvent 

The PSR-14 event \TYPO3\CMS\Core\Core\Event\BootCompletedEvent is fired on every request when TYPO3 has been fully booted, right after all configuration files have been added.

This event complements the AfterTcaCompilationEvent which is executed after TCA configuration has been assembled.

Use cases for this event include running extension's code which needs to be executed at any time and needs TYPO3's full configuration including all loaded extensions.

Example 

EXT:my_extension/Classes/Bootstrap/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Bootstrap\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Core\Event\BootCompletedEvent;

#[AsEventListener(
    identifier: 'my-extension/boot-completed',
)]
final readonly class MyEventListener
{
    public function __invoke(BootCompletedEvent $e): void
    {
        // Do your magic
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

API 

class BootCompletedEvent
Fully qualified name
\TYPO3\CMS\Core\Core\Event\BootCompletedEvent

Executed when TYPO3 has fully booted (after all ext_tables.php files have been processed)

isCachingEnabled ( )
Returns
bool

BeforeCountriesEvaluatedEvent 

New in version 13.3

The PSR-14 event \TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent allows to modify the list of countries provided by the \TYPO3\CMS\Core\Country\CountryProvider .

This event allows to to add, remove and alter countries from the list used by the provider class itself and ViewHelpers like the Form.countrySelect ViewHelper <f:form.countrySelect>.

Example: Add a new country to the country selectors 

The following event listener adds a new country, 'Magic Kingdom' with alpha 2 code 'XX' and alpha 3 code 'XXX'.

EXT:my_extension/Classes/Country/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent;

final readonly class EventListener
{
    #[AsEventListener(identifier: 'my-extension/before-countries-evaluated')]
    public function __invoke(BeforeCountriesEvaluatedEvent $event): void
    {
        $countries = $event->getCountries();
        unset($countries['BS']);
        $countries['XX'] = new Country(
            'XX',
            'XYZ',
            'Magic Kingdom',
            '987',
            '🔮',
            'Kingdom of Magic and Wonders',
        );
        $event->setCountries($countries);
    }
}
Copied!

New in version 13.0

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener has been introduced to tag a PHP class as an event listener. Alternatively, or if you need to be compatible with older TYPO3 versions, you can also register an event listener via the Configuration/Services.yaml file. Switch to an older version of this page for an example or have a look at the section Implementing an event listener in your extension.

As the localized names for the countries are defined in file EXT:core/Resources/Private/Language/Iso/countries.xlf, this language file needs to be extended via locallangXMLOverride:

EXT:my_extension/ext_localconf.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']
['EXT:core/Resources/Private/Language/Iso/countries.xlf'][]
    = 'EXT:my_extension/Resources/Private/Language/countries.xlf';
Copied!

You can now override the language file in the path defined above:

EXT:my_extension/Resources/Private/Language/countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

And add additional translations for German:

EXT:my_extension/Resources/Private/Language/de.countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" target-language="de" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
                <target>Magisches Königreich</target>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
                <target>Königreich der Magie und Wunder</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

And Klingon:

EXT:my_extension/Resources/Private/Language/tlh.countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" target-language="tlh" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
                <target>‘oHtaHghach wo’</target>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
                <target>‘oHtaHghach je Dojmey wo’</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

API of event BeforeCountriesEvaluatedEvent 

class BeforeCountriesEvaluatedEvent
Fully qualified name
\TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent

Event dispatched before countries are evaluated by CountryProvider.

getCountries ( )
Returns
\Country[]
setCountries ( array $countries)
param $countries

the countries

AlterTableDefinitionStatementsEvent 

The PSR-14 event \TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent allows to intercept the CREATE TABLE statement from all loaded extensions.

Example 

API 

class AlterTableDefinitionStatementsEvent
Fully qualified name
\TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent

Event to intercept the "CREATE TABLE" statement from all loaded extensions.

The $sqlData variable holds a RAW Array of definitions from each file found.

addSqlData ( ?mixed $data)
param $data

the data

getSqlData ( )
Returns
array
setSqlData ( array $sqlData)
param $sqlData

the sqlData

AppendLinkHandlerElementsEvent 

The PSR-14 event \TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent is fired so listeners can intercept and add elements when checking links within the soft reference parser.

Example 

API 

class AppendLinkHandlerElementsEvent
Fully qualified name
\TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent

Event fired so listeners can intercept add elements when checking links within the SoftRef parser

getLinkParts ( )
Returns
array
getContent ( )
Returns
string
getElements ( )
Returns
array
getIdx ( )
Returns
int
getTokenId ( )
Returns
string
setLinkParts ( array $linkParts)
param $linkParts

the linkParts

setContent ( string $content)
param $content

the content

setElements ( array $elements)
param $elements

the elements

addElements ( array $elements)
param $elements

the elements

isResolved ( )
Returns
bool

IsTableExcludedFromReferenceIndexEvent 

The PSR-14 event \TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent allows to intercept, if a certain table should be excluded from the Reference Index. There is no need to add tables without a definition in $GLOBALS['TCA'] since the reference index only handles those.

Example 

API 

class IsTableExcludedFromReferenceIndexEvent
Fully qualified name
\TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent

Event to intercept if a certain table should be excluded from the Reference Index.

There is no need to add tables without a definition in $GLOBALS['TCA'] since ReferenceIndex only handles those.

getTable ( )
Returns
string
markAsExcluded ( )
isTableExcluded ( )
Returns
bool
isPropagationStopped ( )
Returns
bool

AfterRecordLanguageOverlayEvent 

New in version 12.0

This event serves as a replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] .

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent can be used to modify the actual translated record (if found) to add additional information or perform custom processing of the record.

Example 

API 

class AfterRecordLanguageOverlayEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent

Event which is fired after a record was translated (or tried to be localized).

getTable ( )
Returns
string
getRecord ( )
Returns
array
getLanguageAspect ( )
Returns
\TYPO3\CMS\Core\Context\LanguageAspect
setLocalizedRecord ( ?array $localizedRecord)
param $localizedRecord

the localizedRecord

getLocalizedRecord ( )
Returns
?array
overlayingWasAttempted ( )

Determines if the overlay functionality happened, thus, returning the lo

Returns
bool

BeforePageIsRetrievedEvent 

New in version 13.0

This event serves as a more powerful replacement for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'] hook.

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent allows to modify the resolving of page records within \TYPO3\CMS\Core\Domain\PageRepository->getPage().

It can be used to alter the incoming page ID or to even fetch a fully-loaded page object before the default TYPO3 behaviour is executed, effectively bypassing the default page resolving.

Example 

EXT:my_extension/Classes/Domain/Access/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Page;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent;
use TYPO3\CMS\Core\Domain\Page;

#[AsEventListener(
    identifier: 'my-extension/my-custom-page-resolver',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforePageIsRetrievedEvent $event): void
    {
        if ($event->getPageId() === 13) {
            $event->setPageId(42);
            return;
        }

        if ($event->getContext()->getPropertyFromAspect('language', 'id') > 0) {
            $event->setPage(new Page(['uid' => 43]));
        }
    }
}
Copied!

New in version 13.0

API 

class BeforePageIsRetrievedEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent

Event which is fired before a page (id) is being resolved from PageRepository.

Allows to change the corresponding page ID, e.g. to resolve a different page with custom overlaying, or to fully resolve the page on your own.

getPage ( )
Returns
?\TYPO3\CMS\Core\Domain\Page
setPage ( \TYPO3\CMS\Core\Domain\Page $page)
param $page

the page

hasPage ( )
Returns
bool
getPageId ( )
Returns
int
setPageId ( int $pageId)
param $pageId

the pageId

skipGroupAccessCheck ( )
respectGroupAccessCheck ( )
isGroupAccessCheckSkipped ( )
Returns
bool
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context

BeforePageLanguageOverlayEvent 

New in version 12.0

This event serves as a replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPageOverlay'] .

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\BeforePageLanguageOverlayEvent is a special event which is fired when TYPO3 is about to do the language overlay of one or multiple pages, which could be one full record or multiple page IDs. This event is fired only for pages and in-between the events BeforeRecordLanguageOverlayEvent and AfterRecordLanguageOverlayEvent.

Example 

API 

class BeforePageLanguageOverlayEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\BeforePageLanguageOverlayEvent

Event which is fired before a single page or a list of pages are about to be translated (or tried to be localized).

getPageInput ( )
Returns
array
setPageInput ( array $pageInput)
param $pageInput

the pageInput

getPageIds ( )
Returns
array
setPageIds ( array $pageIds)
param $pageIds

the pageIds

getLanguageAspect ( )
Returns
\TYPO3\CMS\Core\Context\LanguageAspect
setLanguageAspect ( \TYPO3\CMS\Core\Context\LanguageAspect $languageAspect)
param $languageAspect

the languageAspect

BeforeRecordLanguageOverlayEvent 

New in version 12.0

This event serves as replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] .

The PSR-14 event \TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent can be used to modify information (such as the LanguageAspect or the actual incoming record from the database) before the database is queried.

Example: Change the overlay type to "on" (connected) 

In this example, we will change the overlay type to "on" (connected). This may be necessary if your site is configured with free mode, but you have a record type that has languages connected.

EXT:my_extension/Classes/Domain/Language/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Language;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent;

#[AsEventListener(
    identifier: 'my-extension/before-record-language-overlay',
)]
final readonly class MyEventListener
{
    public function __invoke(BeforeRecordLanguageOverlayEvent $event): void
    {
        if ($event->getTable() !== 'tx_myextension_domain_model_record') {
            return;
        }

        $currentLanguageAspect = $event->getLanguageAspect();
        $newLanguageAspect = new LanguageAspect(
            $currentLanguageAspect->getId(),
            $currentLanguageAspect->getContentId(),
            LanguageAspect::OVERLAYS_ON,
            $currentLanguageAspect->getFallbackChain(),
        );

        $event->setLanguageAspect($newLanguageAspect);
    }
}