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.

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 APIs are first and foremost documented inside of the source scripts. It would be impossible to maintain documentation at more than one location given the fact that things change and sometimes fast. This chapter describes the most important 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.

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

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

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

Authentication

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.

Changed in version 12.0

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 .

    EXT:my_extension/Classes/Controller/MyController.php
    <?php
    
    use TYPO3\CMS\Core\Security\RequestToken;
    use TYPO3\CMS\Fluid\View\StandaloneView;
    
    final class MyController
    {
        private StandaloneView $view;
    
        public function showFormAction()
        {
            // creating new request token with scope 'my/process' and hand over to view
            $requestToken = RequestToken::create('my/process');
            $this->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!

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 any information, the autoloader falls back to the classmap autoloading like in non-Composer mode.

Troubleshooting

Dump the class loading information manually via composer dumpautoload 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.

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.

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 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 filemounts 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 > Access 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 filemount

New in version 11.3

Starting with TYPO3 v11.3 it is possible to create a new filemount via the context menu of the folder.

To create a new filemount 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 Filemount, then give the new filemount a name. The entry point is already set.

It is also possible to create a filemount 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

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

@typo3/backend/page-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.

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

Changed in version 12.0

Previously, module functions could be added to modules such as Web > Info or Web > Template via the now removed global TBE_MODULES_EXT array.

These are now registered as third-level modules with the backend module configuration API.

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 EXT: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;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Type\Bitmask\Permission;

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

        $buttonBar = $view->getDocHeaderComponent()->getButtonBar();
        $this->addPopulateButton($buttonBar);
        $this->addShortCutButton($buttonBar);
        $this->addReloadButton($buttonBar);

        $view->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
        $view->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:blog_example/Resources/Private/Language/Module/locallang_mod.xlf:mlang_tabs_tab'),
            $context,
        );

        $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
        $pageRecord = BackendUtility::readPageAccess(
            $this->pageUid,
            $permissionClause,
        );
        if ($pageRecord) {
            $view->getDocHeaderComponent()->setMetaInformation($pageRecord);
        }
    }

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

        $this->modifyDocHeaderComponent($view);
        $view->setFlashMessageQueue($this->getFlashMessageQueue());

        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|int $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
getView ( )

Deprecated: since v12, will be removed in v13.

Returns
\TYPO3\CMS\Fluid\View\StandaloneView
setContent ( string $content)

Deprecated: since v12, will be removed in v13.

param $content

the content

Returns
self
renderContent ( )

Deprecated: since v12, will be removed in v13. Remove together with $legacyView property and Templates/Module.html.

Returns
string
getBodyTag ( )

Deprecated: since v12, will be removed in v13.

Returns the current body tag.

Returns
string
registerModuleMenu ( string $moduleMenuIdentifier)

Deprecated: since v12, will be removed in v13.

Generates the Menu for things like Web->Info

param $moduleMenuIdentifier

the moduleMenuIdentifier

Returns
self
makeDocHeaderModuleMenu ( array $additionalQueryParams = [])

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

param $additionalQueryParams

the additionalQueryParams, default: []

Returns
self
getDynamicTabMenu ( array $menuItems, string $domId, int $defaultTabIndex = 1, bool $collapsible = false, bool $wrapContent = true, bool $storeLastActiveTab = true)

Deprecated: since v12, will be removed in v13.

Creates a tab menu where the tabs or collapsible are rendered with bootstrap markup

param $menuItems

Tab elements, each element is an array with "label" and "content"

param $domId

DOM id attribute, will be appended with an iteration number per tab.

param $defaultTabIndex

Default tab to open (for toggle <=0). Value corresponds to integer-array index + 1(index zero is "1", index "1" is 2 etc.). A value of zero (or something non-existingwill result in no default tab open., default: 1

param $collapsible

If set, the tabs are rendered as headers instead over each sheet. Effectively this meansthere is no tab menu, but rather a foldout/fold-in menu., default: false

param $wrapContent

If set, the content is wrapped in div structure which provides a padding and borderstyle. Set this FALSE to get unstyled content pane with fullsize content area., default: true

param $storeLastActiveTab

If set, the last open tab is stored in local storage and will be re-open again.If you don't need this feature, e.g. for wizards like import/export you candisable this behaviour., default: true

Returns
string
header ( string $text, bool $inlineEdit = true)

Deprecated: since v12, will be removed in v13.

Returns the header-bar in the top of most backend modules Closes section if open.

param $text

The text string for the header

param $inlineEdit

Whether the header should be editable (e.g. page title), default: true

Return description

HTML content

Returns
string
isUiBlock ( )

Deprecated: since v12, will be removed in v13.

Returns
bool

Example: Create and use a ModuleTemplate in an Extbase Controller

The following example is extracted from the example Extbase extension EXT: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);

        $this->modifyDocHeaderComponent($view);
        $view->setFlashMessageQueue($this->getFlashMessageQueue());

        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

New in version 12.4

With TYPO3 v12.4 sudo mode has been changed to a generic configuration for backend routes (and implicitly modules).

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!

Migration from RequireJS

RequireJS is shimmed to prefer ES6 modules if available, allowing any extension to ship ES6 modules by providing an import map configuration in Configuration/JavaScriptModules.php while providing full backward compatibility support for extensions that load modules via RequireJS.

Existing RequireJS modules can load new ES6 modules via a bridge that prefers ES6 modules over traditional RequireJS AMD modules. This allows extensions authors to migrate to ES6 without breaking dependencies that previously loaded a module of that extension via RequireJS.

Registering modules via $pageRenderer->requireJsModules will still work in TYPO3 v12. These modules will be loaded after modules registered via $pageRenderer->javaScriptModules. Extensions that use $pageRenderer->requireJsModules will work as before but trigger a PHP E_USER_DEPRECATED error.

If your extension wants to support both TYPO3 v11 and v12 you can keep the RequireJS version and remove it upon switching to TYPO3 v13.

If you want to prevent deprecation warnings you can also implement both RequireJS (with a version switch) and native ECMAScript v6/v11 (ES6) modules. This approach is recommended if you are working with TypeScript and the JavaScript will be generated anyway.

Migrate your JavaScript from the AMD module format to native ES6 modules and register your configuration in EXT:my_extension/Configuration/JavaScriptModules.php, also see Loading ES6 for more information:

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

return [
    'dependencies' => ['core', 'backend'],
    'imports' => [
        '@vendor/my-extension/' => 'EXT:my_extension/Resources/Public/JavaScript/',
    ],
];
Copied!

Then use TYPO3\CMS\Core\Page\PageRenderer::loadJavaScriptModules() instead of TYPO3\CMS\Core\Page\PageRenderer::loadRequireJsModule() to load the ES6 module:

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

use TYPO3\CMS\Core\Information\Typo3Version;

// ...

$typo3Version = new Typo3Version();
if ($typo3Version->getMajorVersion() > 11) {
    $this->pageRenderer->loadJavaScriptModule(
        '@vendor/my-extension/my-example.js',
    );
} else {
    // keep RequireJs for TYPO3 below v12.0
    $this->pageRenderer->loadRequireJsModule(
        'TYPO3/CMS/MyExtension/MyExample',
    );
}
Copied!

In Fluid templates includeJavaScriptModules is to be used instead of includeRequireJsModules.

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

Fluid template for TYPO3 v12 and above

<f:be.pageRenderer
    includeJavaScriptModules="{
         0: '@myvendor/my-extension/example.js'
      }"
/>
Copied!
Fluid template for TYPO3 v11 and below
<f:be.pageRenderer
    includeRequireJsModules="{
        0: '@vendor/my-extension/my-example.js'
    }"
/>
Copied!

RequireJS (Deprecated)

Credits

The complete documentation about RequireJS was inspired by the blog post of Andreas Fernandez.

Overview

Use RequireJS in your own extension

To be able to use RequireJS at all, some prerequisites must be fulfilled:

  • Your extension must have a Resources/Public/JavaScript directory. That directory is used for autoloading the modules stored in your extension.
  • Each module has a namespace and a module name. The namespace is TYPO3/CMS/<EXTKEY>, <EXTKEY> is your extension key in UpperCamelCase, e.g. foo_bar = FooBar
  • The namespace maps automatic to your Resources/Public/JavaScript directory
  • The filename is the modulename + .js

Think about what's the purpose of the module. You can only write one module per file (anything else is bad practice anyway) A complete example: TYPO3/CMS/FooBar/MyMagicModule is resided in EXT:foo_bar/Resources/Public/JavaScript/MyMagicModule.js

Every AMD (Asynchronous Module Definition) is wrapped in the same construct:

define([], function() {
   // your module logic here
});
Copied!

This is the "container" of the module. It holds the module logic and takes care of dependencies.

TYPO3 defines in its own modules an object to hold the module logic in properties and methods. The object has the same name as the module. In our case "MyMagicModule":

define([], function() {
   var MyMagicModule = {
      foo: 'bar'
   };

   MyMagicModule.init = function() {
     // do init stuff
   };

   // To let the module be a dependency of another module, we return our object
   return MyMagicModule;
});
Copied!

Dependency handling

Let us try to explain the dependency handling with the most used JS lib: jQuery

To prevent the "$ is undefined" error, you should use the dependency handling of RequireJS. To get jQuery working in your code, use the following line:

define(['jquery'], function($) {
   // in this callback $ can be used
});
Copied!

The code above is very easy to understand:

  1. every dependency in the array of the first argument
  2. will be injected in the callback function at the same position

Let us combine jQuery with our own module from the Extension example

define(['jquery', 'TYPO3/CMS/FooBar/MyMagicModule'], function($, MyMagicModule) {
   // $ is our jQuery object
   // MyMagicModule is the object, which is returned from our own module
   if(MyMagicModule.foo == 'bar'){
      MyMagicModule.init();
   }
});
Copied!

Loading your own or other RequireJS modules

In case you use the ready event, you may wonder how to use the module. Answer: it depends! If you use Fluid's f:be.pageRenderer view helper add the argument includeRequireJsModules:

<f:be.pageRenderer includeRequireJsModules="{
   0:'TYPO3/CMS/FooBar/Wisdom'
}" />
Copied!

However, if you don't use Fluid you may use PageRenderer in your controller:

$pageRenderer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Page\PageRenderer::class);
$pageRenderer->loadRequireJsModule('TYPO3/CMS/FooBar/MyMagicModule');
Copied!

Bonus: loadRequireJsModule takes a second argument $callBackFunction which is executed right after the module was loaded. The callback function must be wrapped within function() {}:

$pageRenderer->loadRequireJsModule(
   'TYPO3/CMS/FooBar/MyMagicModule',
   'function() { console.log("Loaded own module."); }'
);
Copied!

Shim Library to Use it as Own RequireJS Modules

Not all javascript libraries are compatible with RequireJS. In the rarest cases, you can adjust the library code to be AMD or UMD compatible. So you need to configure RequireJS to accept the library.

In RequireJS you can use requirejs.config({}) to shim a library. In TYPO3 the RequireJS config will be defined in the PageRenderer:

EXT:some_extension/Classes/SomeClass.php
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Page\PageRenderer;

$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->addRequireJsConfiguration(
   [
      'paths' => [
         'jquery' => 'sysext/core/Resources/Public/JavaScript/Contrib/jquery/',
         'plupload' => PathUtility::getPublicResourceWebPath(
            'EXT:some_extension/node_modules/plupload/js/plupload.full.min'),
      ],
      'shim' => [
         'deps' => ['jquery'],
         'plupload' => ['exports' => 'plupload'],
      ],
   ]
);
Copied!

In this example we configure RequireJS to use plupload. The only dependency is jquery. We already have jquery in the TYPO3 Core extension.

After the shim and export of plupload it is usable in the dependency handling:

define([
   'jquery',
   'plupload'
], function($, plupload) {
   'use strict';
});
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-bs-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-bs-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-bs-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.

Code examples:

// Show/ hide the wizard
MultiStepWizard.show();
MultiStepWizard.dismiss();

// Add a slide to the wizard
MultiStepWizard.addSlide(
    identifier,
    stepTitle,
    content,
    severity,
    progressBarTitle,
    function() {
    ...
    }
);

// Lock/ unlock navigation buttons
MultiStepWizard.lockNextStep();
MultiStepWizard.unlockNextStep();
MultiStepWizard.lockPrevStep();
MultiStepWizard.unlockPrevStep();
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.

import $ from 'jquery';

$(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

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 request = new AjaxRequest('https://example.org/my-endpoint');
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');

const qs = {
  foo: 'bar',
  bar: {
    baz: ['foo', 'bencer']
  }
};
request = request.withQueryArguments(qs);

// 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
  • immediate (boolean) - if true, the event listener is called right when the event started

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!

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.

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.

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 {
                    name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.3
                    colPos = 3
                    colspan = 1
                  }
                }
              }
              2 {
                columns {
                  1 {
                    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 {
                name = Header
                colspan = 3
                colPos = 1
              }
            }
          }
          2 {
            columns {
              1 {
                name = Main
                colspan = 2
                colPos = 0
              }
              2 {
                name = Aside
                rowspan = 2
                colPos = 2
              }
            }
          }
          3 {
            columns {
              1 {
                name = Main Left
                colPos = 5
              }
              2 {
                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 Content mapping.

Reference implementations of backend layouts

The extension EXT: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 EXT:container integrates the grid layout concept also to regular content elements.

The extension EXT: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',
                    ],
                ],
            ],
        );

        // ... do some other stuff
    }
}
Copied!

Sudo mode

New in version 12.4

Starting with TYPO3 v12.4 a the sudo mode, like for 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().

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 current backend module

Deprecated since version 12.0

The method ->modAccess() of the backend user object is deprecated. Use ModuleProvider->accessGranted() from the ModuleProvider API instead.

$MCONF is module configuration and the key $MCONF['access'] determines the access scope for the module. This function call will check if the $GLOBALS['BE_USER'] is allowed to access the module and if not, the function will exit with an error message.

EXT:some_extension/Classes/Controller/SomeModuleController.php
$GLOBALS['BE_USER']->modAccess($MCONF);
Copied!

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

Deprecated since version 12.1

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 :html:data-contextmenu-uid="10"`
  • data-context="tree" with :html:data-contextmenu-context="tree"`

to be compatible with TYPO3 v12+. To be compatible with TYPO3 v11 and v12 keep the previous attributes.

Using the deprecated JavaScript API will trigger a warning in the console and will stop working with TYPO3 v13.

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, filemounts, 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!

Migration from binding this to context menu

Deprecated since version 12.0

Due to historical reasons, a context menu item was bound to this in its callback action which was used to access the context menu item's dataset. With TYPO3 v12.0 the invocation of the assigned callback actions is adapted to pass the dataset as the third argument.

Binding the context menu item to this in the callback is marked as deprecated.

To access data attributes, use the dataset argument passed as the third argument in the context menu callback action.

Before migration (TYPO3 v11)
ContextMenuActions.renameFile(table, uid)
{
  const actionUrl = $(this).data('action-url');
  top.TYPO3.Backend.ContentContainer.setUrl(
    actionUrl + '&target=' + encodeURIComponent(uid) + '&returnUrl='
    + ContextMenuActions.getReturnUrl()
  );
}
Copied!
After migration (TYPO3 v12)
ContextMenuActions.renameFile(table, uid, dataset)
{
  const actionUrl = dataset.actionUrl;
  top.TYPO3.Backend.ContentContainer.setUrl(
    actionUrl + '&target=' + encodeURIComponent(uid) + '&returnUrl='
    + ContextMenuActions.getReturnUrl()
  );
}
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

The LoginProviderInterface contains only one method:

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

The view

As mentioned above, the render method gets the Fluid StandaloneView as first parameter. You have to set the template path and filename using the methods of this object. 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).

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

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!

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

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.

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.
imagesizes

groups: lowlevel

  • Cache for imagesizes.
  • It should only be cleared manually, if you know what you are doing.
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

New in version 12.4.2

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

Frontend API

All frontends must implement the API defined in interface TYPO3\CMS\Core\Cache\Frontend\FrontendInterface. All operations on a specific cache must be done with these methods. The frontend object of a cache is the main object any cache manipulation is done with, usually the assigned backend object should not be used directly.

Method

Description

getIdentifier

Returns the cache identifier.

getBackend

Returns the backend instance of this cache. It is seldom needed in usual code.

set

Sets/overwrites an entry in the cache.

get

Returns the cache entry for the given identifier.

has

Checks for existence of a cache entry. Do no use this prior to get() since get() returns NULL if an entry does not exist.

remove

Removes the entry for the given identifier from the cache.

flushByTag

Flushes all cache entries which are tagged with the given tag.

collectGarbage

Calls the garbage collection method of the backend. This is important for backends which are unable to do this internally (like the DB backend).

isValidEntryIdentifier

Checks if a given identifier is valid.

isValidTag

Checks if a given tag is valid.

requireOnce

PhpFrontend only Requires a cached PHP file directly.

Available Frontends

Currently two different frontends are implemented. The main difference are the data types which can be stored using a specific frontend.

Variable Frontend

Strings, arrays and objects are accepted by this frontend. Data is serialized before it is passed to the backend.

PHP Frontend

This is a special frontend to cache PHP files. It extends the string frontend with the method requireOnce() which allows PHP files to be require()'d if a cache entry exists. This can be used by extensions to cache and speed up loading of calculated PHP code and becomes handy if a lot of reflection and dynamic PHP class construction is done.

A backend to be used in combination with the PHP frontend must implement the interface TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface. Currently the file backend and the simple file backend fulfill this requirement.

Cache backends

A variety of storage backends exists. They have different characteristics and can be used for different caching needs. The best backend depends on a given server setup and hardware, as well as cache type and usage. A backend should be chosen wisely, as a wrong decision could end up actually slowing down a TYPO3 installation.

Backend API

All backends must implement at least interface TYPO3\CMS\Core\Cache\Backend\BackendInterface. All operations on a specific cache must be done with these methods. There are several further interfaces that can be implemented by backends to declare additional capabilities. Usually, extension code should not handle cache backend operations directly, but should use the frontend object instead.

Method

Description

setCache

Reference to the frontend which uses the backend. This method is mostly used internally.

set

Save data in the cache.

get

Load data from the cache.

has

Checks if a cache entry with the specified identifier exists.

remove

Remove a cache entry with the specified identifier.

flush

Remove all cache entries.

collectGarbage

Does garbage collection.

flushByTag

TaggableBackendInterface only Removes all cache entries which are tagged by the specified tag.

findIdentifiersByTag

TaggableBackendInterface only Finds and returns all cache entry identifiers which are tagged by the specified tag.

requireOnce

PhpCapableBackendInterface only Loads PHP code from the cache and require_onces it right away.

freeze

FreezableBackendInterface only Freezes this cache backend.

isFrozen

FreezableBackendInterface only Tells if this backend is frozen.

Common Options

Option

Description

Mandatory

Type

Default

defaultLifetime

Default lifetime in seconds of a cache entry if it is not specified for a specific entry on set()

No

integer

3600

Database Backend

This is the main backend suitable for most storage needs. It does not require additional server daemons nor server configuration.

The database backend does not automatically perform garbage collection. Instead the Scheduler garbage collection task should be used.

It stores data in the configured database (usually MySQL) and can handle large amounts of data with reasonable performance. Data and tags are stored in two different tables, every cache needs its own set of tables. In terms of performance the database backend is already pretty well optimized and should be used as default backend if in doubt. This backend is the default backend if no backend is specifically set in the configuration.

The Core takes care of creating and updating required database tables "on the fly".

For caches with a lot of read and write operations, it is important to tune the MySQL setup. The most important setting is innodb_buffer_pool_size. A generic goal is to give MySQL as much RAM as needed to have the main table space loaded completely in memory.

The database backend tends to slow down if there are many write operations and big caches which do not fit into memory because of slow harddrive seek and write performance. If the data table grows too big to fit into memory, it is possible to compress given data transparently with this backend, which often shrinks the amount of needed space 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. The compression should not be enabled for caches which are read or written multiple times during one request.

InnoDB Issues

The database backend for MySQL uses InnoDB tables. Due to the nature of InnoDB, deleting records does not reclaim the actual disk space. E.g. if the cache uses 10GB, cleaning it will still keep 10GB allocated on the disk even though phpMyAdmin will show 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 by any mean that you should skip the scheduler task. Deleting records still improves performance.

Options

Option

Description

Mandatory

Type

Default

compression

Whether or not data should be compressed with gzip. This can reduce size of the cache data table, but incurs CPU overhead for compression and decompression.

No

boolean

false

compressionLevel

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)

No

integer from -1 to 9

-1

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.

Warning and Design Constraints

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 some 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 which should be removed and they will not be deleted. This results in old data delivered by the cache. Additionally, there is currently no implementation of the 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 sort of namespacing. To distinguish entries of multiple caches from each other, every entry is prefixed with the cache name. This can lead to very long runtimes if a big cache needs to be flushed, because every entry has to be handled separately and it is not possible to just truncate the whole cache with one call as this would clear the whole memcached data which might even hold non TYPO3 related entries.

Because of the mentioned drawbacks, the memcached backend should be used with care or in situations where cache integrity is not important or if a cache has no need to use tags at all. Currently, the memcache backend implements the TaggableBackendInterface, so the implementation does allow tagging, even if it is not advised to used this backend together with heavy tagging.

Options

Option

Description

Mandatory

Type

Default

servers

Array of used memcached servers. At least one server must be defined. Each server definition is a string, allowed 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

Yes

array

 

compression

Enable memcached internal data compression. Can be used to reduce memcached memory consumption, but adds additional compression / decompression CPU overhead on the related memcached servers.

No

boolean

false

Redis Backend

Redis is a key-value storage/database. In contrast to memcached, it allows structured values. Data is stored in RAM but it allows persistence 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 helps 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 known to be extremely fast but very memory hungry. The implementation is an option for big caches with lots of data because most important operations perform O(1) in proportion to the number of (redis) keys. This basically means that the access to an entry in a cache with a million entries is not slower than to a cache with only 10 entries, at least if 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, but there is one caveat.

TYPO3 caches should be separated in case the same keys are used. This applies to the pages and pagesection caches. Both use "tagIdents:pageId_21566" for a page with an id of 21566. How you separate them is more of a system administrator decision. 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). The separation has the additional advantage that 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 the cache identifier for each cache), 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

Option

Description

Mandatory

Type

Default

hostname

IP address or name of redis server to connect to.

No

string

127.0.0.1

port

Port of the redis daemon.

No

integer

6379

persistentConnection

Activate a persistent connection to redis server. This could be a benefit under high load cloud setups.

No

boolean

false

database

Number of the database to store entries. Each cache should use its own database, otherwise all caches sharing a database are 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.

No

integer

password

Password used to connect to the redis instance if the redis server needs authentication.

No

string

 

compression

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.

No

boolean

false

compressionLevel

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)

No

integer from -1 to 9

-1

Redis server configuration

This section is about the configuration on the Redis server, not the client.

For the 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 in place which ensures that all entries which are related, 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 maxmemory-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 to the file system. The lifetime and tags are added after the data part in the same file.

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, the get and set operations have low overhead. The file backend is not very good with tagging and does not scale well with the number of tags. Do not use this backend if cached data has many tags.

Options

Option

Description

Mandatory

Type

Default

cacheDirectory

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 can be selected, too. Every cache should be assigned its own directory, otherwise flushing of one cache would flush all other caches within the same directory as well.

No

string

typo3temp/cache/

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 can not be tagged and flushed by tag. This improves the performance if cache entries do not need such tagging. The TYPO3 Core uses this backend for its central Core cache (that hold 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.

The garbage collection is implemented for this backend and should be called to clean up hard disk space or memory.

Options

Option

Description

Mandatory

Type

Default

dataSourceName

Data source name for connecting to the database. Examples:

  • mysql:host=localhost;dbname=test
  • sqlite:/path/to/sqlite.db
  • sqlite::memory

Yes

string

 

username

Username for the database connection.

No

string

 

password

Password to use for the database connection.

No

string

 

Transient Memory Backend

The transient memory backend stores data in a PHP array. It is only valid for one request. This becomes handy if code logic needs to do expensive calculations or must look up identical information from a database over and over again during its execution. In this case it is useful to store the data in an array once and lookup the entry from the cache for consecutive calls to get rid of the otherwise additional overhead. Since caches are available system wide and shared between Core and extensions they can profit from each other if they need the same information.

Since the data is stored directly in memory, this backend is the quickest backend available. 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 becomes handy in development context to practically "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.

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.

Changed in version 11.4

Starting with v11.4 the formerly used PHP function ExtensionManagementUtility::makeCategorizable() is deprecated and removed with v12.0. Use a TCA field of the type category instead.

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.

Console commands (CLI)

It is possible to run TYPO3 scripts from the command line. This functionality can be used to set up cron jobs, for example.

TYPO3 uses Symfony commands API for writing CLI (command line interface) commands. These commands can also be run from the TYPO3 scheduler if this option is not disabled in the Configuration/Services.yaml.

The starting point for the commands differs depending on the type of your TYPO3 installation.

  • For installations with Composer, the starting point is the project folder, which also contains the composer.json file of the project. The CLI commands usually start with vendor/bin/typo3.
  • For Classic mode installations (without Composer), the starting point is usually the web root, so CLI commands start with typo3/sysext/core/bin/typo3.

Run a command from the command line

You can list the available commands by calling:

vendor/bin/typo3
Copied!
typo3/sysext/core/bin/typo3
Copied!

For example, you can clear all caches by calling:

vendor/bin/typo3 cache:flush
Copied!
typo3/sysext/core/bin/typo3 cache:flush
Copied!

Show help for the command:

vendor/bin/typo3 cache:flush -h
Copied!
typo3/sysext/core/bin/typo3 cache:flush -h
Copied!

Running the command 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 1. Register the command.

Create a custom command

See the Tutorial: Create a console command for details on how to create commands.

DataHandler usage

Using the DataHandler in a CLI command requires backend authentication. See Using the DataHandler in a Symfony command for more information.

Read more

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 EXT: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
 
completion
Dump the shell completion script
help
Display help for a command
list
List commands
setup
Setup TYPO3 via CLI using environment variables, CLI options or interactive
backend
 
backend:lock
Lock the TYPO3 Backend
backend:resetpassword
Trigger a password reset for a backend user
backend:unlock
Unlock the TYPO3 Backend
backend:user:create
Create a backend user
cache
 
cache:flush
Flush TYPO3 caches.
cache:warmup
Warmup TYPO3 caches.
cleanup
 
cleanup:deletedrecords
Permanently deletes all records marked as "deleted" in the database.
cleanup:flexforms
Updates all database records which have a FlexForm field and the XML data does not match the chosen datastructure.
cleanup:localprocessedfiles
Delete processed files and their database records.
cleanup:missingrelations
Find all record references pointing to a non-existing record
cleanup:orphanrecords
Find and delete records that have lost their connection with the page tree.
cleanup:previewlinks
Clean up expired preview links from shared workspace previews.
cleanup:versions
Find all versioned records and possibly cleans up invalid records in the database.
extension
 
extension:list
Shows the list of extensions available to the system
extension:setup
Set up extensions
impexp
 
impexp:export
Exports a T3D / XML file with content of a page tree
impexp:import
Imports a T3D / XML file with content into a page tree
language
 
language:update
Update the language files of all activated extensions
mailer
 
mailer:spool:send
Sends emails from the spool
messenger
 
messenger:consume
Consume messages
redirects
 
redirects:checkintegrity
Check integrity of redirects
redirects:cleanup
Cleanup old redirects periodically for given constraints like days, hit count or domains.
referenceindex
 
referenceindex:update
Update the reference index of TYPO3
scheduler
 
scheduler:execute
Execute given Scheduler tasks.
scheduler:list
List all Scheduler tasks.
scheduler:run
Start the TYPO3 Scheduler from the command line.
site
 
site:list
Shows the list of sites available to the system
site:show
Shows the configuration of the specified site
syslog
 
syslog:list
Show entries from the sys_log database table of the last 24 hours.
upgrade
 
upgrade:list
List available upgrade wizards.
upgrade:run
Run upgrade wizard. Without arguments all available wizards will be run.
workspace
 
workspace:autopublish
Publish a workspace with a publication date.

completion

completion Back to list
Dump the shell completion script
Usage
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
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:

completion  | sudo tee /etc/bash_completion.d/typo3
Copied!

Or dump the script to a local file and source it:

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/completion )"
Copied!

help

help Back to list
Display help for a command
Usage
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
Help

The help command displays help for a given command:

help list
Copied!

You can also output the help in other formats by using the --format option:

help --format=xml list
Copied!

To display the list of available commands, please use the list command.

list

list Back to list
List commands
Usage
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
Help

The list command lists all commands:

list
Copied!

You can also display the commands for a specific namespace:

list test
Copied!

You can also output the information in other formats by using the --format option:

list --format=xml
Copied!

It's also possible to get raw list of commands (useful for embedding command runner):

list --raw
Copied!

setup

setup Back to list
Setup TYPO3 via CLI using environment variables, CLI options or interactive
Usage
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
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" \
./setup --force

Copied!

backend:lock

backend:lock Back to list
Lock the TYPO3 Backend
Usage
backend:lock [<redirect>]
Copied!
Arguments

redirect

redirect
If set, then the TYPO3 Backend will redirect to the locking state (only used when locking the TYPO3 Backend
Help

Lock the TYPO3 Backend

backend:resetpassword

backend:resetpassword Back to list
Trigger a password reset for a backend user
Usage
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
Help

Trigger a password reset for a backend user

backend:unlock

backend:unlock Back to list
Unlock the TYPO3 Backend
Usage
backend:unlock
Copied!
Help

Unlock the TYPO3 Backend

backend:user:create

backend:user:create Back to list
Create a backend user
Usage
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
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 \
./backend:user:create --no-interaction
Copied!

cache:flush

cache:flush Back to list
Flush TYPO3 caches.
Usage
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"
Help

This command can be used to clear the caches, for example after code updates in local development and after deployments.

cache:warmup

cache:warmup Back to list
Warmup TYPO3 caches.
Usage
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"
Help

This command is useful for deployments to warmup caches during release preparation.

cleanup:deletedrecords

cleanup:deletedrecords Back to list
Permanently deletes all records marked as "deleted" in the database.
Usage
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
Help

Traverse page tree and find and flush deleted records. If you want to get more detailed information, use the --verbose option.

cleanup:flexforms

cleanup:flexforms Back to list
Updates all database records which have a FlexForm field and the XML data does not match the chosen datastructure.
Usage
cleanup:flexforms [-p|--pid PID] [-d|--depth DEPTH] [--dry-run]
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 be updated, but only show the output which records would have been updated.
Value
None allowed
Default value
false
Help

Traverse page tree and find and clean/update records with dirty FlexForm values. If you want to get more detailed information, use the --verbose option.

cleanup:localprocessedfiles

cleanup:localprocessedfiles Back to list
Delete processed files and their database records.
Usage
cleanup:localprocessedfiles [--dry-run] [-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

--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
Help

If you want to get more detailed information, use the --verbose option.

cleanup:missingrelations

cleanup:missingrelations Back to list
Find all record references pointing to a non-existing record
Usage
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
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.

cleanup:orphanrecords

cleanup:orphanrecords Back to list
Find and delete records that have lost their connection with the page tree.
Usage
cleanup:orphanrecords [--dry-run]
Copied!
Options

--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
Help

Assumption: All actively used records on the website from TCA configured tables are located in the page tree exclusively.

All records managed by TYPO3 via the TCA array configuration has to belong to a page in the page tree, either directly or indirectly as a version of another record. VERY TIME, CPU and MEMORY intensive operation since the full page tree is looked up!

Automatic Repair of Errors: - Silently deleting the orphaned records. In theory they should not be used anywhere in the system, but there could be references. See below for more details on this matter.

Manual repair suggestions: - Possibly re-connect orphaned records to page tree by setting their "pid" field to a valid page id. A lookup in the sys_refindex table can reveal if there are references to an orphaned record. If there are such references (from records that are not themselves orphans) you might consider to re-connect the record to the page tree, otherwise it should be safe to delete it.

If you want to get more detailed information, use the --verbose option.

cleanup:previewlinks

Clean up expired preview links from shared workspace previews.
Usage
cleanup:previewlinks
Copied!
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.

cleanup:versions

cleanup:versions Back to list
Find all versioned records and possibly cleans up invalid records in the database.
Usage
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
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.

extension:list

extension:list Back to list
Shows the list of extensions available to the system
Usage
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
Help

Shows the list of extensions available to the system

extension:setup

extension:setup Back to list
Set up extensions
Usage
extension:setup [-e|--extension EXTENSION]
Copied!
Options

--extension

--extension / -e
Only set up extensions with given key
Value
Required (multiple)
Default value
[]
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.

impexp:export

impexp:export Back to list
Exports a T3D / XML file with content of a page tree
Usage
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", "-1": "Expanded tree", "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
Help

Exports a T3D / XML file with content of a page tree

impexp:import

impexp:import Back to list
Imports a T3D / XML file with content into a page tree
Usage
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
Help

Imports a T3D / XML file with content into a page tree

language:update

language:update Back to list
Update the language files of all activated extensions
Usage
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
[]
Help

Update the language files of all activated extensions

mailer:spool:send

mailer:spool:send Back to list
Sends emails from the spool
Usage
mailer:spool:send [--message-limit MESSAGE-LIMIT] [--time-limit TIME-LIMIT] [--recover-timeout RECOVER-TIMEOUT]
Copied!
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
Help

Sends emails from the spool

messenger:consume

messenger:consume Back to list
Consume messages
Usage
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
Help

The messenger:consume command consumes messages and dispatches them to the message bus.

php messenger:consume <receiver-name>

To receive from multiple transports, pass each name:

php messenger:consume receiver1 receiver2
Copied!

Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):

php messenger:consume <receiver-name> --queues=fasttrack
Copied!

redirects:checkintegrity

redirects:checkintegrity Back to list
Check integrity of redirects
Usage
redirects:checkintegrity [<site>]
Copied!
Arguments

site

site
If set, then only pages of a specific site are checked
Help

Check integrity of redirects

redirects:cleanup

redirects:cleanup Back to list
Cleanup old redirects periodically for given constraints like days, hit count or domains.
Usage
redirects:cleanup [-d|--domain [DOMAIN]] [-s|--statusCode [STATUSCODE]] [-a|--days [DAYS]] [-c|--hitCount [HITCOUNT]] [-p|--path [PATH]] [-t|--creationType [CREATIONTYPE]]
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
Help

Cleanup old redirects periodically for given constraints like days, hit count or domains.

referenceindex:update

referenceindex:update Back to list
Update the reference index of TYPO3
Usage
referenceindex:update [-c|--check]
Copied!
Options

--check

--check / -c
Only check the reference index of TYPO3
Value
None allowed
Default value
false
Help

Update the reference index of TYPO3

scheduler:execute

scheduler:execute Back to list
Execute given Scheduler tasks.
Usage
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
[]
Help

Execute given Scheduler tasks.

scheduler:list

scheduler:list Back to list
List all Scheduler tasks.
Usage
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
Help

List all Scheduler tasks.

scheduler:run

scheduler:run Back to list
Start the TYPO3 Scheduler from the command line.
Usage
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
Help

If no parameter is given, the scheduler executes any tasks that are overdue to run. Call it like this: typo3/sysext/core/scheduler:run --task=13 -f

site:list

site:list Back to list
Shows the list of sites available to the system
Usage
site:list
Copied!
Help

Shows the list of sites available to the system

site:show

site:show Back to list
Shows the configuration of the specified site
Usage
site:show <identifier>
Copied!
Arguments

identifier

identifier
The identifier of the site
Help

Shows the configuration of the specified site

syslog:list

syslog:list Back to list
Show entries from the sys_log database table of the last 24 hours.
Usage
syslog:list
Copied!
Help

Prints a list of recent sys_log entries. If you want to get more detailed information, use the --verbose option.

upgrade:list

upgrade:list Back to list
List available upgrade wizards.
Usage
upgrade:list [-a|--all]
Copied!
Options

--all

--all / -a
Include wizards already done.
Value
None allowed
Default value
false
Help

List available upgrade wizards.

upgrade:run

upgrade:run Back to list
Run upgrade wizard. Without arguments all available wizards will be run.
Usage
upgrade:run [<wizardName>]
Copied!
Arguments

wizardName

wizardName
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.

workspace:autopublish

workspace:autopublish Back to list
Publish a workspace with a publication date.
Usage
workspace:autopublish
Copied!
Help

Some workspaces can have an auto-publish publication date to put all "ready to publish" content online on a certain date.

Tutorial

Create a console command from scratch

A console command is always situated in an extension. If you want to create one, kickstart a custom extension or use your sitepackage extension.

Creating a basic command

The extension kickstarter "Make" offers a convenient console command that creates a new command in an extension of your choice: Create a new console command with "Make". You can use "Make" to create a console command even if your extension was created by different means.

This command can be found in the Examples extension.

1. Register the command

New in version 12.4.8

Register the command in Configuration/Services.yaml by adding the service definition for your class as tag console.command:

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

  T3docs\Examples\:
    resource: '../Classes/*'
    exclude: '../Classes/Domain/Model/*'

  T3docs\Examples\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!

The following attributes are available:

command
The name under which the command is available.
description
Give a short description. It will be displayed in the list of commands and the help information of the command.
schedulable
By default, a command can be used in the scheduler, too. This can be disabled by setting schedulable to false.
hidden
A command can be hidden 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.

2. Create the command class

Create a class called DoSomethingCommand extending \Symfony\Component\Console\Command\Command.

EXT:examples/Classes/Command/DoSomethingCommand.php
<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project. [...]
 */

namespace T3docs\Examples\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

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
    {
        // Do awesome stuff
        return Command::SUCCESS;
    }
}
Copied!

The following two methods should be overridden by your class:

configure()
As the name would suggest, allows to configure the command. The method allows to add a help text and/or define arguments and options.
execute()
Contains the logic when executing the command. Must return an integer. It is considered best practice to return the constants Command::SUCCESS or Command::FAILURE.

3. Run the command

The above example can be run via command line:

vendor/bin/typo3 examples:dosomething
Copied!
typo3/sysext/core/bin/typo3 examples:dosomething
Copied!

The command will return without a message as it does nothing but stating it succeeded.

Use the PHP attribute to register commands

New in version 12.4.8

CLI commands can be registered by setting the attribute \Symfony\Component\Console\Attribute\AsCommand on the command class. When using this attribute there is no need to register the command in the Services.yaml file.

The example above can also be registered this way:

EXT:my_extension/Classes/Command/MyCommand.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;

#[AsCommand(
    name: 'examples: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
    {
        // Do awesome stuff
        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:

Add an optional argument and an optional option to your command:

Class T3docs\Examples\Command\CreateWizardCommand
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

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'
            );
    }
}
Copied!

This command takes one optional argument wizardName and one optional option, which can be passed on the command line:

vendor/bin/typo3 examples:createwizard [-b] [wizardName]
Copied!
typo3/sysext/core/bin/typo3 examples:createwizard [-b] [wizardName]
Copied!

This argument can be retrieved with $input->getArgument(), the options with $input->getOption(), for example:

Class T3docs\Examples\Command\CreateWizardCommand
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 T3docs\Examples\Exception\InvalidWizardException;

final class CreateWizardCommand extends 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;
    }
}
Copied!

User interaction on the console

You can create a SymfonyStyle console user interface from the $input and $output parameters to the execute() function:

EXT:examples/Classes/Command/CreateWizardCommand.php
use Symfony\Component\Console\Style\SymfonyStyle;

final class CreateWizardCommand extends Command
{
    protected function execute(
        InputInterface $input,
        OutputInterface $output
    ): int {
        $io = new SymfonyStyle($input, $output);
        // do some user interaction
        return Command::SUCCESS;
    }
}
Copied!

The $io variable can then be used to generate output and prompt for input:

Class T3docs\Examples\Command\CreateWizardCommand
use Symfony\Component\Console\Style\SymfonyStyle;
use T3docs\Examples\Exception\InvalidWizardException;

final class CreateWizardCommand extends Command
{
    private function doMagic(
        SymfonyStyle $io,
        mixed $wizardName,
        bool $bruteForce
    ) {
        $io->comment('Trying to create wizard ' . $wizardName . '...');
        if ($wizardName === null) {
            $wizardName = (string)$io->ask(
                'Enter the wizard\'s name (e.g. "Gandalf the Grey")',
                'Lord Voldermort'
            );
        }
        if (!$bruteForce && $wizardName === 'Oz') {
            $io->error('The Wizard of Oz is not allowed. Use --brute-force to allow it.');
            throw new InvalidWizardException();
        }
        $io->success('The wizard ' . $wizardName . ' was created');
    }
}
Copied!

Dependency injection in console commands

You can use dependency injection (DI) in console commands by constructor injection or method injection:

Class T3docs\Examples\Command\MeowInformationCommand
use Psr\Log\LoggerInterface;
use T3docs\Examples\Http\MeowInformationRequester;

final class MeowInformationCommand extends Command
{
    public function __construct(
        private readonly MeowInformationRequester $requester,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }
}
Copied!

Initialize backend user

A backend user can be initialized with this call inside execute() method:

EXT:some_extension/Classes/Command/DoBackendRelatedThingsCommand.php
use TYPO3\CMS\Core\Core\Bootstrap;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

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 DataHandler or other backend permission handling related tasks.

Simulating a Frontend Request in TYPO3 Commands

When executing TYPO3 commands in the CLI, there is no actual 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.

The Challenge

In a web request, TYPO3 automatically provides various objects that influence link generation:

  • ContentObjectRenderer (cObj): Processes TypoScript-based rendering, including link generation.
  • page Attribute: Holds the current page context.
  • PageInformation Object: Provides additional metadata about the current page.
  • Router: Ensures proper URL resolution.
  • FrontendTypoScriptFactory (was part of TSFE at the time): Collects TypoScript and provides settings like linkAccessRestrictedPages and typolinkLinkAccessRestrictedPages.

One critical limitation is that the ContentObjectRenderer (cObj) is only available when a TypoScript-based content element, such as FLUIDTEMPLATE, is rendered. Even if cObj is manually instantiated in a CLI command, its data array remains empty, meaning it lacks the context of a real tt_content record. As a result, TypoScript properties like field = my_field or data = my_data will not work as expected.

Similarly, the FrontendTypoScriptFactory is not automatically available in CLI. If CLI-generated links should respect settings like linkAccessRestrictedPages, it would have to be manually instantiated and configured.

A Minimal Request Example

In some cases, a minimal request configuration may be sufficient, such as when generating simple links or using FluidEmail. The following example demonstrates how to set up a basic CLI request with applicationType and site attributes:

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: 'examples:dosomething',
    description: 'A command that does nothing and always succeeds.',
    aliases: ['examples:dosomethingalias'],
)]
class DoSomethingCommand 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!

More information

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!

By using ExtensionUtility::PLUGIN_TYPE_PLUGIN as fifth parameter is is also possible to add the plugin as a list type. See CType vs list_type plugins.

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!

By using 'list_type' as second parameter is is also possible to add the plugin as a list type. See CType vs list_type plugins.

Plugins are a specific type of content elements. Plugins use the CType='list'. Each plugin has its own plugin type, which is used in the database field tt_content.list_type. The list_type could be understood as subtype of CType.

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.

CType vs list_type plugins

Historically it was common to add plugins as a list type to the content element types. In this case the column CType is set to 'list' for all plugins while the field list_type contains the key of the actual plugin.

As different plugins need different fields in the backend form this let to the creation of all type of complicated TCA constructs to influence the behaviour of backend forms for plugins.

The existence of the list_type also made a separate layer of content element definitions in the TypoScript necessary.

Therefore the list_type complicates registration and configuration of plugins while it poses no advantages. Therefore it is recommended to always use the CType for new plugin types while the list_type is retained for now for backward compatibility.

If you are refactoring the plugins of your extension, for example while getting rid of switchable controller actions it is recommended to migrate your plugins to use the CType. You should then supply a upgrade wizard for easy migration for your users.

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 EXT: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 examples_newcontentelement 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);
defined('TYPO3') or die();

// Adds the content element to the "Type" dropdown
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        // title
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_newcontentelement_title',
        // plugin signature: extkey_identifier
        'value' => 'myextension_newcontentelement',
        // icon identifier
        'icon' => 'content-text',
        // group
        'group' => 'default',
        // description
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_newcontentelement_description',
    ],
    'textmedia',
    'after',
);

// Adds the content element icon to TCA typeicon_classes
$GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes']['myextension_newcontentelement'] = 'content-text';

// ...
Copied!

Now the new content element is available in the backend form. However it currently contains no fields but the CType field.

CType dropdown in tt_content

About the icon

You can either use an existing icon from the TYPO3 core or register your own icon using the Icon API. In this example we use the icon content-text, the same icon as the Regular Text Element uses.

Add it to the new content element wizard

Content elements in the New Content Element Wizard are easier to find for editors. It is therefore advised to add the new content element to this wizard (via page TSconfig).

EXT:my_extension/Configuration/page.tsconfig
mod.wizards.newContentElement.wizardItems {
  // add the content element to the tab "common"
  common {
    elements {
      myextension_newcontentelement {
        iconIdentifier = content-text
        title = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_newcontentelement_title
        description = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myextension_newcontentelement_description
        tt_content_defValues {
          CType = myextension_newcontentelement
        }
      }
    }
    show := addToList(myextension_newcontentelement)
  }
}
Copied!

Changed in version 12.0

Content element wizard with the new content element

Content element wizard with the new content element

The content element wizard configuration is described in detail in Add content elements to the Content Element Wizard.

Configure the backend form

Then you need to 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);
defined('TYPO3') or die();

/* See above
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(...);
*/

// Configure the default backend fields for the content element
$GLOBALS['TCA']['tt_content']['types']['myextension_newcontentelement'] = [
    'showitem' => '
            --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
               --palette--;;general,
               header; Internal title (not displayed),
               bodytext;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:bodytext_formlabel,
            --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
               --palette--;;hidden,
               --palette--;;access,
         ',
    'columnsOverrides' => [
        'bodytext' => [
            'config' => [
                'enableRichtext' => true,
                'richtextConfiguration' => 'default',
            ],
        ],
    ],
];
Copied!

Now the backend form for the new content elements looks like this:

The backend form

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/setup.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/
}
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_newcontentelement =< lib.contentElement
  myextension_newcontentelement {
    templateName = NewContentElement
  }
}
Copied!

The lib.contentElement path is defined in file EXT:fluid_styled_content/Configuration/TypoScript/Helper/ContentElement.typoscript. 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 NewContentElement.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/NewContentElement.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 richtext enabled field bodytext, using the html saved by the richtext editor:

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. Since we saved the content of bodytext in the richt text editor we have to run it through f:format.html to resolve all links and other formatting. 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 EXT: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 EXT: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
        # Before TYPO3 v12.1 you have to give the fully-qualified class name of the processor
        # dataProcessing.10 = T3docs\Examples\DataProcessing\CustomCategoryProcessor
        # Since TYPO3 v12.1 one can also use a (in Services.yaml) configured alias
        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 $contentObjectConfiguration The configuration of Content Object
     * @param array $processorConfiguration The configuration of this processor
     * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
     * @return array 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 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 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.

  3. Table and field have a subtype_value_field TCA setting

    If your table and field have a subtype_value_field TCA setting (like tt_content.list_type for example) and you want to register a preview renderer that applies only when that value is selected (assume, when a certain plugin type is selected and you can't match it with the "type" of the record alone):

    $GLOBALS['TCA'][$table]['types'][$type]['previewRenderer'][$subType]
        = MyVendor\MyExtension\Preview\MyPreviewRenderer::class;
    Copied!

    Where $type is, for example, list (indicating a plugin) and $subType is the value of the list_type field when the type of plugin you want to target is selected as plugin type.

Add content elements to the Content Element Wizard

The content element wizard opens when a new content element is created. It can be fully configured using ref:Page TSconfig <t3tsref:pagetsconfig>.

Our extension key is example and the name of the content element or plugin is registration.

  1. Create page TSconfig

    EXT:example/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig
    mod.wizards {
        newContentElement.wizardItems {
            plugins {
                elements {
                    example_registration {
                        iconIdentifier = example-registration
                        title = Registration Example
                        description = Create a registration form
                        tt_content_defValues {
                            CType = list
                            list_type = example_registration
                        }
                    }
                }
            }
        }
    }
    Copied!

    You may want to replace title and description from above, using language files for translation, for example:

    title = LLL:EXT:example/Resources/Private/Language/locallang.xlf:registration_title
    description = LLL:EXT:example/Resources/Private/Language/locallang.xlf:registration_description
    Copied!
  2. Include TSconfig

    EXT:example/Configuration/page.tsconfig
    @import 'EXT:example/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig'
    Copied!

    This always includes the above page TSconfig. It is better practice to make this configurable by registering this file as static page TSconfig.

  3. Register your icon

    EXT:example/Configuration/Icons.php
    <?php
    
    return [
       // use same identifier as used in TSconfig for icon
       'example-registration' => [
          'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
          'source' => 'EXT:example/Resources/Public/Icons/example-registration.svg',
       ],
    ];
    Copied!
  4. After clearing cache, create a new content element

    After clearing the cache via Admin Tools > Maintenance or the command vendor/bin/typo3 cache:flush you should now see the icon, title and description you just added!

    Content element wizard with the new content element

    Content element wizard with the new content element

Add your plugin or content element to a different tab

The above example adds your plugin to the tab "Plugin" in the content element wizard. You can add it to one of the other existing tabs or create a new one.

If you add it to any of the other tabs (other than plugins), you must add the name to show as well:

EXT:example/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig
mod.wizards.newContentElement.wizardItems.common {
    elements {
        example_registration {
            iconIdentifier = example-registration
            title = Example title
            description = Example description
            tt_content_defValues {
                CType = list
                list_type = example_registration
            }
        }
    }
    show := addToList(example_registration)
}
Copied!

When you look at existing page TSconfig in the Info module, you may notice that show has been set to include all for the Plugins tab:

show = *
Copied!

Create a new tab

See the bootstrap_package for an example of creating a new tab Interactive and adding elements to it:

EXT:bootstrap_package/Configuration/TsConfig/Page/ContentElement/Categories.tsconfig
mod.wizards.newContentElement.wizardItems {
    interactive.header = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_group.interactive
}
Copied!
EXT:bootstrap_package/Configuration/TsConfig/Page/ContentElement/Element/Accordion.tsconfig
mod.wizards.newContentElement.wizardItems.interactive {
    elements {
        accordion {
            iconIdentifier = content-bootstrappackage-accordion
            title = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_element.accordion
            description = LLL:EXT:bootstrap_package/Resources/Private/Language/Backend.xlf:content_element.accordion.description
            tt_content_defValues {
                CType = accordion
            }
        }
    }
    show := addToList(accordion)
}
Copied!

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.

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 reporting 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')
# reporting:
#   inheritDefault: true
#   mutations:
#    - mode: "append"
#      directive: "img-src"
#      sources:
#        - "cdn.example.com"
#
Copied!

For new installations security.backend.enforceContentSecurityPolicy is enabled by default.

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

New in version 12.4.20

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 $GLOBALS['TSFE']->sys_language_content . 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.

TypoScript aspect

The TypoScript aspect can be used to manipulate/check whether TemplateRendering is forced.

The TypoScript aspect, \TYPO3\CMS\Core\Context\TypoScriptAspect contains the following property:

forcedTemplateParsing

forcedTemplateParsing
Call
$this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');

Returns, whether TypoScript template parsing is forced.

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 (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 with databases, allowing them to perform database operations through an object-oriented API while ensuring compatibility across different database systems.

Within the TYPO3 backend rows of database tables are usually represented as Database records and configured in TCA (Table Configuration Array).

In Extbase based 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.
  • However, this "connection per table" approach is limited: In the above example, if a join query is executed that spans different connections, an exception will be thrown. It is up to the administrator to group the affected tables to the same connection in those cases, or a developer should implement fallback logic to suppress the join().

Database structure

Types of tables

The database tables used by TYPO3 can be roughly divided into two categories:

Internal tables

Tables that are used internally by the system and are invisible to backend users (for example, be_sessions, sys_registry, cache-related tables). In the Core extension, there are often dedicated PHP APIs for managing entries in these tables, for instance, the caching framework API.

Managed tables

Tables that can be managed via the TYPO3 backend are shown in the Web > List module and can be edited using the FormEngine.

Requirements

There are certain requirements for such managed tables:

  • The table must be configured in the global TCA array, for example:

    • table name
    • features that are required
    • fields of the table and how they should be rendered in the backend
    • relations to other tables

    and so on.

  • The table must contain at least these fields:

    • uid - an auto-incremented integer and primary key for the table, containing the unique ID of the record in the table.
    • pid - an integer pointing to the uid of the page (record from pages table) to which the record belongs.

    The fields are created automatically when the table is associated with a TCA configuration.

Typical fields

  • A title field holding the title of the record as seen in the backend.
  • A description field holding a description displayed in the Web > List view.
  • A crdate field holding the creation time of the record.
  • A tstamp field holding the last modification time of the record.
  • A sorting field holding an order when records are sorted manually.
  • A deleted field which tells TYPO3 that the record is deleted (actually implementing a "soft delete" feature; records with a deleted field are not truly deleted from the database).
  • A hidden or disabled field for records which exist but should not be used (for example, disabled backend users, content not visible in the frontend).

The "pages" table

The pages table has a special status: It is the backbone of TYPO3, as it provides the hierarchical page structure into which all other records managed by TYPO3 are placed. All other managed tables in TYPO3 have a pid field that points to a uid record in this table. Thus, each managed table record in TYPO3 is always placed on exactly one page in the page tree. This makes the pages table the mother of all other managed tables. It can be seen as a directory tree with all other table records as files.

Standard pages are literally website pages in the frontend. But they can also be storage spaces in the backend, similar to folders on a hard disk. For each record, the pid field contains a reference to the page where that record is stored. For pages, the pid fields behaves as a reference to their parent pages.

The special "root" page has some unique properties: its pid is 0 (zero), it does not exist as a row in the pages table, only users with administrative rights can access records on it, and these records must be explicitly configured to reside in the root page - usually, table records can only be created on a real page.

MM relations

When tables are connected via a many-to-many relationship, another table must store these relations. Examples are the table storing relations between categories and categorized records ( sys_category_record_mm) or the table storing relations between files and their various usages in pages, content elements, etc. ( sys_file_reference). The latter is an interesting example, because it actually appears in the backend, although only as part of inline records.

Other tables

The internal tables which are not managed through the TYPO3 backend serve various purposes. Some of the most common are:

  • Cache: If a cache is defined to use the database as a cache backend, TYPO3 automatically creates and manages the relevant cache tables.
  • System information: There are tables that store information about sessions, both frontend and backend ( fe_sessions and be_sessions respectively), a table for a central registry ( sys_registry) and some others.

All these tables are not subject to the uid/ pid constraint mentioned above, but they may have such fields if it is convenient for some reason.

There is no way to manage such tables through the TYPO3 backend, unless a specific module provides some form of access to them. For example, the System > Log module provides an interface for browsing records from the sys_log table.

Upgrade table and field definitions

Each extension in TYPO3 can provide the ext_tables.sql file that defines which tables and fields the extension needs. Gathering all ext_tables.sql files thus defines the complete set of tables, fields and indexes of a TYPO3 instance to unfold its full functionality. The Analyze Database Structure section in the Admin Tools > Maintenance backend module can compare the defined set with the current active database schema and shows options to align these two by adding fields, removing fields and so on.

When you upgrade to newer versions of TYPO3 or upgrade an extension, the data definition of tables and fields may have changed. The database structure analyzer detects such changes.

When you install a new extension, any change to the database is performed automatically. When upgrading to a new major version of TYPO3, you should normally go through the upgrade wizard, whose first step is to perform all necessary database changes:

The Upgrade Wizard indicating that the database needs updates

If you want to perform minor updates, update extensions or generally check the functionality of your system, you can go to Admin Tools > Maintenance > Analyze Database Structure:

The Database analyzer is part of the maintenance area

This tool collects the information from all ext_tables.sql files of all active extensions and compares them with the current database structure. Then it proposes to perform the necessary changes, grouped by type:

  • creating new tables
  • adding new fields to existing tables
  • altering existing fields
  • dropping unused tables and fields

You can choose which updates you want to perform. You can even decide not to create new fields and tables, although that will very likely break your installation.

The ext_tables.sql files

As mentioned above, all data definition statements are stored in files named ext_tables.sql, which may exist in any extension.

The peculiarity is that these files may not always contain a complete and valid SQL data definition. For example, the "dashboard" system extension defines a new table for storing dashboards:

EXT:dashboard/ext_tables.sql
CREATE TABLE be_dashboards (
    identifier varchar(120) DEFAULT '' NOT NULL,
    title varchar(120) DEFAULT '' NOT NULL,
    widgets text
);
Copied!

This is a complete and valid SQL data definition. However, the community extension "news" extends the tt_content table with additional fields. It also provides these changes in the form of a SQL CREATE TABLE statement:

EXT:news/ext_tables.sql
CREATE TABLE tt_content (
    tx_news_related_news int(11) DEFAULT '0' NOT NULL,
    KEY index_newscontent (tx_news_related_news)
);
Copied!

The classes which take care of assembling the complete SQL data definition will compile all the CREATE TABLE statements for a given table and turn them into a single CREATE TABLE statement. If the table already exists, missing fields are isolated and ALTER TABLE statements are proposed instead.

This means that as an extension developer you should always only have CREATE TABLE statements in your ext_tables.sql files, the system will handle them as needed.

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 add more complex sorting you can use ->add('orderBy', 'FIELD(eventtype, 0, 4, 1, 2, 3)', true), remember to quote properly!

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 ->add('groupBy', $sql, $append), remember to quote properly!

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(0) 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 (Starting with TYPO3 v13.0, ->setMaxResults(null) will have to be used).

add()

The ->add() method appends or replaces a single, generic query part. It can be used as a low level call when more specific calls do not provide enough freedom to express parts of statements:

EXT:my_extension/Classes/Domain/Repository/MyRepository.php
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_language');
$queryBuilder
    ->select('*')
    ->from('sys_language')
    ->add('orderBy', 'FIELD(eventtype, 0, 4, 1, 2, 3)');
Copied!

Read how to correctly instantiate a query builder with the connection pool.

Remarks:

  • The first argument is the SQL part. One of: select, from, set, where, groupBy, having or orderBy.
  • The second argument is the (properly quoted!) SQL segment of this part.
  • The optional third boolean argument specifies whether the SQL fragment should be appended ( true) or replace a possibly existing SQL part of this name ( false, default).

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.

execute(), executeQuery() and executeStatement()

Changed in version 11.5

The widely used ->execute() method has been split into:

  • ->executeQuery() returning a \Doctrine\DBAL\Result instead of a \Doctrine\DBAL\Statement and
  • ->executeStatement() returning the number of affected rows.

Although ->execute() still works for backwards compatibility, you should prefer to use ->executeQuery() for SELECT and COUNT statements and ->executeStatement() for INSERT, UPDATE and DELETE queries.

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()

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()

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(self::TABLE_NAME);
    }
}
Copied!

Read how to instantiate a connection with the connection pool.

Remarks:

  • ->lastInsertId($tableName) takes the table name as first argument. Although it is optional, you should always specify the table name for Doctrine DBAL compatibility with engines like PostgreSQL.
  • If the name of the auto increment field is not uid, the second argument must be specified with the name of that field. For simple TYPO3 tables, uid is fine and the argument can be omitted.

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

ExpressionBuilder class is responsible to dynamically create SQL query parts.

It takes care building query conditions while ensuring table and column names are quoted within the created expressions / SQL fragments. It is a facade to the actual Doctrine ExpressionBuilder.

The ExpressionBuilder is used within the context of the QueryBuilder to ensure queries are being build based on the requirements of the database platform 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

Changed in version 11.5.10

The andX() and orX() methods are deprecated and replaced by and() and or() to match with Doctrine DBAL, which deprecated these methods.

  • ->and() conjunction
  • ->or() disjunction

Combine multiple single expressions with AND or OR. Nesting is possible, both methods are variadic and accept any number of arguments, which are all combined. However, it usually makes little sense to pass zero or only one argument.

Example to find 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` = 'list')
        //     AND (
        //        (`tt_content`.`list_type` = 'example_pi1')
        //        OR
        //        (`tt_content`.`list_type` = 'example_pi2')
        //     )
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
        $queryBuilder->where(
            $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('list')),
            $queryBuilder->expr()->or(
                $queryBuilder->expr()->eq(
                    'list_type',
                    $queryBuilder->createNamedParameter('example_pi1', Connection::PARAM_STR),
                ),
                $queryBuilder->expr()->eq(
                    'list_type',
                    $queryBuilder->createNamedParameter('example_pi2', 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 various comparison expressions or 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 generated a <left expr> <> <right expr>. Example:

[php] // u.id <> 1 $q->where($q->expr()->neq('u.id', '1'));

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)

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.

Returns
string
notLike ( string $fieldName, ?mixed $value)

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.

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

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.

trim()

class ExpressionBuilder
Fully qualified name
\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
trim ( string $fieldName, int $position, ?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

param $char

Character to be trimmed (defaults to space), default: NULL

Returns
string

Using the ->trim() expression ensures that the 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\BackendWorkspaceRestriction

Deprecated since version 12.1

Use \TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction instead.

Determines the current workspace a backend user is working in and adds a couple of restrictions to select only records of that workspace if the table supports workspace-enabled records.

\TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction

Deprecated since version 12.1

Use \TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction instead.

Restriction to filter records for frontend workspaces preview.

\TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
WorkspaceRestriction has been added to overcome downsides of FrontendWorkspaceRestriction and BackendWorkspaceRestriction. 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().

Unlike \Doctrine\DBAL\Statement returned formerly by ->execute(), a single prepared statement with different values cannot be executed multiple times.

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

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.

Registering a new driver middleware

In this example, the custom driver middleware MyMiddleware is added to the Default connection:

EXT:my_extension/ext_localconf.php | config/system/additional.php
<?php

declare(strict_types=1);

use MyVendor\MyExtension\Database\Log\MyMiddleware;

defined('TYPO3') or die();

$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['myextension_mymiddleware']
    = MyMiddleware::class;
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.

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
These include text, images, videos, or any other element placed within a page. They are stored in table tt_content.
Backend user records
Information about users who have access to the TYPO3 backend. They are stored in table be_users. The backend user groups they are organized in 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.
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.

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.

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 TypoScriptFrontendController->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\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

final class SomeController extends ActionController
{
    public function showAction(ExampleModel $example): ResponseInterface
    {
        // ...

        /** @var TypoScriptFrontendController $frontendController */
        $frontendController = $this->request->getAttribute('frontend.controller');
        $frontendController->addCacheTags([
            sprintf('tx_myextension_example_%d', $example->getUid()),
        ]);

        // ...
    }
}
Copied!

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" or What is dependency injection? by Fabien Potencier. Whenever a class 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 it is used throughout Core and extensions to standardize object initialization. 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 objects, 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.

This chapter not only talks about Symfony DI and its configuration via Services.yaml, but also a bit about services in general, about GeneralUtility::makeInstance() and the SingletonInterface. And since the TYPO3 core already had an object lifecycle management solution with the extbase ObjectManager before Symfony services were implemented, we'll also talk about how to transition away from it towards the core-wide Symfony solution.

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 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.
  • Second, 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 user 'foo'". And this is where dependency injection enters the game: Logging is a huge topic, there are various levels of error, information can be written to various destinations and so on. The little class does not want to deal with all those details, but 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 details". 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 further 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. With TYPO3 v11 the core doesn't use the ObjectManager any more. It is actively deprecated in v11 and thus leads to 'deprecation' level log entries.

With TYPO3 v12 the Extbase ObjectManager is actually gone. Making use of Symfony DI integration still continues. There are still various places in the core to be improved. Further streamlining will be done over time. For instance, the final fate of makeInstance() and the SingletonInterface has not fully been decided on yet. Various tasks remain to be solved in younger TYPO3 developments 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 anyways.

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 thing. The system will fail upon misconfiguration, so frontend and backend may be unreachable.

The container cache entry (at the time of this writing) is not deleted when a backend admin user clicks "Clear all cache" in the backend top toolbar. The only way to force a DI recalculation is using the "Admin tools" -> "Maintenance" -> "Flush Caches" button of the backend embedded Install Tool or the standalone Install Tool (/typo3/install.php) itself. This means: Whenever core or an extension fiddles with DI (or more general "Container") configuration, this cache has to be manually emptied for a running instance by clicking this button. The backend Extension Manager however does empty the cache automatically when loading or unloading extensions. 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). TYPO3 will then recalculate the cache upon the next access, no matter if it's a frontend, a backend or a CLI request.

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 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. If an instance is requested and the object has been created once already, the same instance is returned. Codewise, this is sometimes done by implementing a static getInstance() method that parks the instance in a property. In TYPO3, this can also be achieved by implementing the SingletonInterface, where makeInstance() then stores the object internally. Within containers, this can be done by declaring the object as shared ( shared: true), which is the default. We'll come back to details later. Singletons must not have state - they must act the same way each time they're used, no matter where, how often or when they've been used before. Otherwise the behavior of a singleton object is undefined and will lead to obscure errors.
Service
This is another "not by the book" definition. We use the understanding "What is a service?" from Symfony: In Symfony, everything that is instantiated through the service container (both directly via $container->get() and indirectly via DI) is a service. These are many things - for instance controllers are services, as well as - non static - utilities, repositories and obvious classes like mailers and similar. To emphasize: Not only classes named with a *Service suffix are services but basically anything. It does not matter much if those services are stateless or not. Controllers, for instance, are usually not stateless. (This is just a configuration detail from this point of view.) Note: The TYPO3 Core does not strictly follow this behavior in all cases yet, but it strives to get this more clean over time.
Data object

Data objects are the opposite of services. They are not available through service containers. Here calling $container->has() returns false and they can not be injected. They are instantiated either with new() or GeneralUtility::makeInstance(). Domain models or DTO are a typical example of data objects.

Note: 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

Now that we have a general understanding of what a service and what a data object is, let's turn to usages of services. We will mostly use examples for this.

The general rule is: Whenever your class has a service dependency to another class, one of the following solutions should be used.

When to use Dependency Injection in TYPO3

Class dependencies to services should be injected via constructor injection or setter methods. Where possible, Symfony dependency injection should be used for all cases where DI is required. Non-service "data objects" like Extbase domain model instances or DTOs should be instantiated via \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() if they are non-final and support XCLASSing. For final classes without dependencies plain instantiation via the new keyword must be used.

In some 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.

When dependency injection cannot be used directly yet, create a service class and make it public in the Configuration/Services.yaml. Create an instance of the service class via GeneralUtility::makeInstance(...) you can then use dependency injection in the service class.

Constructor injection

Assume we're writing a controller that renders a list of users. Those users are found using a custom UserRepository, so the repository service is 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!

Here the Symfony container sees a dependency to UserRepository when scanning __construct() of the UserController. Since autowiring is enabled by default (more on that below), an instance of the UserRepository is created and provided when the controller is created. The example uses constructor property promotion and sets the property readonly, so it can not be written a second time.

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 the basically the same result as above: The controller instance has an object of type UserRepository in class property $userRepository. The injection via methods was introduced by Extbase and 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 dependency to the constructor, and then call parent::__construct($logger) to satisfy the dependency of the abstract. 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 since they did not know that.

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.

As a last note on method injection, there is another way to do it: It is possible to use a setFooDependency() method if it has the annotation @required. This second way of method injection however is not used within the TYPO3 framework, should be avoided in general, and is just mentioned here for completeness.

Interface injection

Apart from constructor injection and inject*() method injection, there is another useful dependency injection scenario. Look at this example:

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!

See the difference? We're requesting the injection of an interface and not a class! It works for both constructor and method injection. It forces the service container to look up which specific class is configured as implementation of the interface and inject an instance of it. This is the true heart of dependency injection: A consuming class no longer codes on a specific implementation, but on the signature of the interface. The framework makes sure something is injected that satisfies the interface, the consuming class does not care, it just knows about the interface methods. An instance administrator can decide to configure the framework to inject some different implementation than the default, and that's fully transparent for consuming classes.

Here's an example scenario that demonstrates how you can define the specific implementations that shall be used for an interface type hint:

EXT:my_extension/Controller/MyController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Service\MyServiceInterface;

class MyController
{
    public function __construct(
        private readonly MyServiceInterface $myService,
    ) {}
}
Copied!
EXT:my_extension/Controller/MySecondController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

class MySecondController extends MyController {}
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)
    {
        $this->myService = $myService;
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  # Define the default implementation of an interface
  MyVendor\MyExtension\Service\MyServiceInterface: '@MyVendor\MyExtension\Service\MyDefaultService'

  # Within MySecond- and MyThirdController different implementations for said
  # interface shall be used instead.

  # Version 1: when working with constructor injection
  MyVendor\MyExtension\Controller\MySecondController:
    arguments:
      $service: '@MyVendor\MyExtension\Service\MySecondService'

  # Version 2: when working with method injection
  MyVendor\MyExtension\Controller\MyThirdController:
    calls:
      - method: 'injectMyService'
        arguments:
          $service: '@MyVendor\MyExtension\Service\MyThirdService'
Copied!

Using container->get()

[WIP] Service containers provide two methods to obtain objects, first via $container->get(), and via DI. This is only available for services itself: Classes that are registered as a service via configuration can use injection or $container->get(). DI is supported in two ways: As constructor injection, and as inject*() method injection. They lead to the same result, but have subtle differences. More on that later.

In general, services should use DI (constructor or method injection) to obtain dependencies. This is what you'll most often find when looking at core implementations. However, it is also possible to get the container injected and then use $container->get() to instantiate services. This is useful for factory-like services where the exact name of classes is determined at runtime.

Configuration

Configure dependency injection in extensions

Extensions have to configure their classes to make use of the dependency injection. This can be done in Configuration/Services.yaml. Alternatively, Configuration/Services.php can also be used. 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. This works for constructor injection and inject*() methods. The calculation generates a service initialization code which is cached in the TYPO3 Core cache.

autoconfigure
It is suggested to enable autoconfigure: true as this automatically adds Symfony service tags based on implemented interfaces or base classes. For example, autoconfiguration ensures that classes implementing \TYPO3\CMS\Core\SingletonInterface are publicly available from the Symfony container and marked as shared ( shared: true).
Model exclusion
The path exclusion exclude: '../Classes/Domain/Model/*' excludes your models from the dependency injection container, which means you cannot inject them nor inject dependencies into them. Models are not services and therefore should not require dependency injection. Also, these objects are created by the Extbase persistence layer, which does not support the DI container.

Arguments

In case you turned off autowire or need special arguments, you can configure those as well. This means that you can set autowire: false for an extension, but provide the required arguments via config specifically for the desired classes. This can be done in chronological order or by naming.

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

  MyVendor\MyExtension\UserFunction\ClassA:
    arguments:
      $argA: '@TYPO3\CMS\Core\Database\ConnectionPool'

  MyVendor\MyExtension\UserFunction\ClassB:
    arguments:
      - '@TYPO3\CMS\Core\Database\ConnectionPool'
Copied!

This allows you to inject concrete objects like the Connection:

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

  connection.pages:
    class: 'TYPO3\CMS\Core\Database\Connection'
    factory:
      - '@TYPO3\CMS\Core\Database\ConnectionPool'
      - 'getConnectionForTable'
    arguments:
      - 'pages'

  MyVendor\MyExtension\UserFunction\ClassA:
    public: true
    arguments:
      - '@connection.pages'
Copied!

Now you can access the Connection instance within ClassA. This allows you to execute your queries without further instantiation. For example, this method of injecting objects also works with extension configurations and with TypoScript settings.

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(). However, some classes that need to be public are automatically marked as public due to autoconfigure: true being set. These classes include singletons, as they must be shared with code that uses GeneralUtility::makeInstance() and Extbase controllers.

What to make public

Every class that is instantiated using GeneralUtility::makeInstance() and requires dependency injection must be marked as public. The same goes for instantiation via GeneralUtility::makeInstance() using constructor arguments.

Any other class which requires dependency injection and is retrieved by dependency injection itself can be private.

Instances of \TYPO3\CMS\Core\SingletonInterface and Extbase controllers are automatically marked as public. This allows them to be retrieved using GeneralUtility::makeInstance() as done by TYPO3 internally.

More examples of classes that must be marked as public:

For such classes, an extension can override the global configuration public: false in Configuration/Services.yaml for each affected class:

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

  MyVendor\MyExtension\:
    resource: '../Classes/*'
    exclude: '../Classes/Domain/Model/*'

  MyVendor\MyExtension\UserFunction\ClassA:
    public: true
Copied!

With this configuration, you can use dependency injection in \MyVendor\MyExtension\UserFunction\ClassA when it is created, for example in the context of a USER TypoScript object, which would not be possible if this class were private.

Errors resulting from wrong configuration

If objects that use dependency injection are not configured properly, one or more of the following issues may result. In such a case, check whether the class has to be configured as public: true.

ArgumentCountError is raised on missing dependency injection for 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!

An Error is thrown on missing dependency injection for Method injection, once the dependency is used within the code:

(1/1) Error

Call to a member function methodName() on null
Copied!

Installation-wide configuration

New in version 12.1

One can set up a global service configuration for a project that can be used in multiple project-specific extensions. For example, this way you can alias an interface with a concrete implementation that can be used in several extensions. It is also possible to register project-specific CLI commands without requiring a project-specific extension.

However, this only works - due to security restrictions - if TYPO3 is configured in a way that the project root is outside the document root, which is usually the case in Composer-based installations.

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.

The global service configuration files services.yaml and services.php are now read within the config/system/ path of a TYPO3 project in Composer-based installations.

Example:

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. Then the concrete implementation may change without touching your code. In this example, we use lcobucci/clock for the concrete implementation.

config/system/services.php
<?php

declare(strict_types=1);

use Lcobucci\Clock\SystemClock;
use Psr\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (
    ContainerConfigurator $containerConfigurator,
    ContainerBuilder $containerBuilder,
): void {
    $services = $containerConfigurator->services();
    $services->set(ClockInterface::class)
        ->factory([SystemClock::class, 'fromUTC']);
};
Copied!

The concrete clock implementation is now injected when a type hint to the interface is given:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use Psr\Clock\ClockInterface;

final class MyClass
{
    public function __construct(
        private readonly ClockInterface $clock,
    ) {}

    // ...
}
Copied!

User functions and their restrictions

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().

This method callUserFunction() internally uses the dependency-injection-aware helper GeneralUtility::makeInstance(), which can recognize and inject classes/services that are marked public.

Dependency injection in a XCLASSed class

When extending an existing class (for example, an Extbase controller) using XCLASS and injecting additional dependencies using constructor injection, ensure that a reference to the extended class is added in the Configuration/Services.yaml file of the extending extension, as shown in the example below:

EXT:my_extension/Configuration/Services.yaml
TYPO3\CMS\Belog\Controller\BackendLogController: '@MyVendor\MyExtension\Controller\ExtendedBackendLogController'
Copied!

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 FLUIDTEMPLATE 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

Registering the event listener

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: 'myListener'
        before: 'redirects, 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\AfterMailerInitializationEvent $event) will for example listen on the event AfterMailerInitializationEvent.

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: 'myListener'
        before: 'redirects, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

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/NullMailer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;

final class NullMailer
{
    public function __invoke(AfterMailerInitializationEvent $event): void
    {
        $event->getMailer()->injectMailSettings(['transport' => 'null']);
    }
}
Copied!

An extension can define multiple listeners.

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.

Overriding event listeners

Existing event listeners can be overridden by custom implementations. This can be performed via EXT:my_extension/Configuration/Services.yaml.

For example, a third-party extension listens on the event \TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent with the following code:

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:

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/after-backend-page-render'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(AfterBackendPageRenderEvent $event): void
    {
        $content = $event->getContent() . ' I was here';
        $event->setContent($content);
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-preview-uri'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-page-tree-items'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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';
            }
        }
        $event->setItems($items);
    }
}
Copied!

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

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/after-record-summary-for-localization'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-module-icon'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Module\BeforeModuleCreationEvent;

final 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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-parameters'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/before-search-in-database-record-provider-event-listener'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(BeforeSearchInDatabaseRecordProviderEvent $event): void
    {
        $event->ignoreTable('my_custom_table');
    }
}
Copied!

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[]

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;

final 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!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Listener/ContentUsedOnPage.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Listener;

use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;

final 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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-file-is-selectable'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(IsFileSelectableEvent $event): void
    {
        // Deny selection of "png" images
        if ($event->getFile()->getExtension() === 'png') {
            $event->denyFileSelection();
        }
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/allowed-items'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyAllowedItemsEvent $event): void
    {
        $event->addAllowedItem('someItem');
        $event->removeAllowedItem('anotherItem');
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-button-bar'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyButtonBarEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

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
array
setButtons ( array $buttons)
param $buttons

the buttons

getButtonBar ( )
Returns
\TYPO3\CMS\Backend\Template\Components\ButtonBar

ModifyClearCacheActionsEvent

New in version 11.4

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/toolbar/my-event-listener'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyClearCacheActionsEvent $event): void
    {
        // do magic here
    }
}
Copied!

The cache action array element consists of the following keys and values:

Example cache action array
[
    'id' => 'pages',
    'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesTitle',
    'description' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesDescription',
    'href' => (string)$uriBuilder->buildUriFromRoute('tce_db', ['cacheCmd' => 'pages']),
    'iconIdentifier' => 'actions-system-cache-clear-impact-low'
]
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
array
addCacheActionIdentifier ( string $cacheActionIdentifier)
param $cacheActionIdentifier

the cacheActionIdentifier

setCacheActionIdentifiers ( array $cacheActionIdentifiers)
param $cacheActionIdentifiers

the cacheActionIdentifiers

getCacheActionIdentifiers ( )
Returns
array

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

Registration of the event in your extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-database-query-for-content'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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\Database\Connection;

final 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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-database-query-for-record-list'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyDatabaseQueryForRecordListingEvent $event): void
    {
        $queryBuilder = $event->getQueryBuilder();

        // ... do something ...

        $event->setQueryBuilder($queryBuilder);
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-edit-form-user-access'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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

Registration of an event listener in your extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/add-message'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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\Messaging\FlashMessage;

final class MyEventListener
{
    public function __invoke(ModifyGenericBackendMessagesEvent $event): void
    {
        // Add a custom message
        $event->addMessage(new FlashMessage('My custom message'));
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-imagemanipulation-previewurl'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyImageManipulationPreviewUrlEvent $event): void
    {
        $event->setPreviewUrl('https://example.com/some/preview/url');
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-enabled-controls'
        method: 'modifyEnabledControls'
      - name: event.listener
        identifier: 'my-extension/backend/modify-controls'
        method: 'modifyControls'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

final 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', Icon::SIZE_SMALL)->render()
                . '</a>',
            );
        }
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-link-explanation'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;

final class MyEventListener
{
    public function __construct(
        private readonly 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',
                    Icon::SIZE_SMALL,
                )->render(),
            );
        }
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/link-handlers'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(ModifyLinkHandlersEvent $event): void
    {
        $handler = $event->getLinkHandler('url.');
        $handler['label'] = 'My custom label';
        $event->setLinkHandler('url.', $handler);
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-wizard-items'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/backend/modify-page-module-content'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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 ( )
Returns
\TYPO3\CMS\Backend\Controller\LoginController
getView ( )
Returns
\TYPO3\CMS\Fluid\View\StandaloneView
getPageRenderer ( )
Returns
\TYPO3\CMS\Core\Page\PageRenderer

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-query-for-live-search-event-listener'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final 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!

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.

Usage

An example registration of the events in your extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/recordlist/my-event-listener'
        method: 'modifyRecordActions'
      - name: event.listener
        identifier: 'my-extension/recordlist/my-event-listener'
        method: 'modifyHeaderColumns'
      - name: event.listener
        identifier: 'my-extension/recordlist/my-event-listener'
        method: 'modifyTableActions'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/add-live-search-result-actions-listener'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

final class MyEventListener
{
    private readonly LanguageService $languageService;

    public function __construct(
        private readonly IconFactory $iconFactory,
        LanguageServiceFactory $languageServiceFactory,
        private readonly 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', Icon::SIZE_SMALL))
                ->setUrl((string)$this->uriBuilder->buildUriFromRoute('record_history', [
                    'element' => $resultItem->getExtraData()['table'] . ':' . $resultItem->getExtraData()['uid'],
                ]));
            $resultItem->addAction($showHistoryAction);
        }
    }
}
Copied!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Backend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/preview-rendering-example-ctype'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

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;

final class MyEventListener
{
    public function __invoke(PageContentPreviewRenderingEvent $event): void
    {
        if ($event->getTable() !== 'tt_content') {
            return;
        }

        if ($event->getRecord()['CType'] === 'example_ctype') {
            $event->setPreviewContent('<div>...</div>');
        }
    }
}
Copied!

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
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

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

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'] .

The purpose of the PSR-14 event \TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent is to trigger any kind of action when a backend 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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Authentication\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/after-user-logged-in'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;

final 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!

API

class AfterUserLoggedInEvent
Fully qualified name
\TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent

Event fired after a user has been actively logged in (incl. possible MFA).

Currently only working in BE context, but might get opened up in FE context as well in TYPO3 v13+.

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:

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Authentication\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/process-request-token-listener'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Classes/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent;
use TYPO3\CMS\Core\Security\RequestToken;

final 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!

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Authentication\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/login-attempt-failed'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Authentication/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Authentication\EventListener;

use TYPO3\CMS\Core\Authentication\Event\LoginAttemptFailedEvent;

final 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!

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

New in version 11.4

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

New in version 11.4

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

Registration of the events in the extension's Services.yaml:

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

  MyVendor\MyExtension\Configuration\EventListener\FlexFormParsingModifyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/set-data-structure'
        method: 'setDataStructure'
      - name: event.listener
        identifier: 'my-extension/modify-data-structure'
        method: 'modifyDataStructure'
      - name: event.listener
        identifier: 'my-extension/set-data-structure-identifier'
        method: 'setDataStructureIdentifier'
      - name: event.listener
        identifier: 'my-extension/modify-data-structure-identifier'
        method: 'modifyDataStructureIdentifier'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Configuration/EventListener/FlexFormParsingModifyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Configuration\EventListener;

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;

final 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!

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 $GLOBALS['TCA'] is built to allow to further manipulate $tca.

Side note: It is possible to check against the original TCA as this is stored within $GLOBALS['TCA'] before this event is fired.

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.

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 will stop calling the old event. See also Compatibility with TYPO3 v11 and v12.

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

Compatibility with TYPO3 v11 and v12

Extensions that want to stay compatible with both TYPO3 v11 and v12 and prepare v13 compatibility as much as possible should start listening for the new event as well, and suppress handling of the old event in TYPO3 v12 to not handle things twice.

Example from b13/bolt extension:

Registration of both events in the file Services.yaml:

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

  B13\Bolt\TsConfig\Loader:
    public: true
    tags:
      # Remove when TYPO3 v11 compat is dropped
      - name: event.listener
        identifier: 'add-site-configuration-v11'
        event: TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent
        method: 'addSiteConfigurationCore11'
      # TYPO3 v12 and above
      - name: event.listener
        identifier: 'add-site-configuration'
        event: TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent
        method: 'addSiteConfiguration'
Copied!

Read how to configure dependency injection in extensions.

Handle the old event in TYPO3 v11 only, but skip old event with TYPO3 v12:

EXT:bolt/Classes/TsConfig/Loader.php
<?php

declare(strict_types=1);

namespace B13\Bolt\TsConfig;

use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent as LegacyModifyLoadedPageTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;

class Loader
{
    public function addSiteConfigurationCore11(LegacyModifyLoadedPageTsConfigEvent $event): void
    {
        if (class_exists(ModifyLoadedPageTsConfigEvent::class)) {
            // TYPO3 v12 calls both old and new event. Check for class existence of new event to
            // skip handling of old event in v12, but continue to work with < v12.
            // Simplify this construct when v11 compat is dropped, clean up Services.yaml.
            return;
        }
        $this->findAndAddConfiguration($event);
    }

    public function addSiteConfiguration(ModifyLoadedPageTsConfigEvent $event): void
    {
        $this->findAndAddConfiguration($event);
    }

    protected function findAndAddConfiguration($event): void
    {
        // Business code
    }
}
Copied!

See the complete code on GitHub: Loader.php.

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

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

New in version 11.4

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

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Bootstrap\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/boot-completed'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Classes/Bootstrap/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Bootstrap\EventListener;

use TYPO3\CMS\Core\Core\Event\BootCompletedEvent;

final class MyEventListener
{
    public function __invoke(BootCompletedEvent $e): void
    {
        // Do your magic
    }
}
Copied!

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

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

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

API

class BeforeRecordLanguageOverlayEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent

Event which is fired before a record in a language should be "language overlaid", that is: Finding a translation for a given record.

getTable ( )
Returns
string
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

getLanguageAspect ( )
Returns
\TYPO3\CMS\Core\Context\LanguageAspect
setLanguageAspect ( \TYPO3\CMS\Core\Context\LanguageAspect $languageAspect)
param $languageAspect

the languageAspect

RecordAccessGrantedEvent

New in version 12.0

This event serves as replacement for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] .

The PSR-14 event \TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent can be used to either define whether a record access is granted for a user, or to modify the record in question. In case the $accessGranted property is set (either true or false), the defined settings are directly used, skipping any further event listener as well as any further evaluation.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Domain\Access\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/set-access-granted'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Domain/Access/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Access;

use TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent;

final class MyEventListener
{
    public function __invoke(RecordAccessGrantedEvent $event): void
    {
        // Manually set access granted
        if (
            $event->getTable() === 'my_table' &&
            ($event->getRecord()['custom_access_field'] ?? false)
        ) {
            $event->setAccessGranted(true);
        }

        // Update the record to be checked
        $record = $event->getRecord();
        $record['some_field'] = true;
        $event->updateRecord($record);
    }
}
Copied!

API

class RecordAccessGrantedEvent
Fully qualified name
\TYPO3\CMS\Core\Domain\Access\RecordAccessGrantedEvent

Event to modify records to be checked against "enableFields".

Listeners are able to grant access or to modify the record itself to continue to use the native access check functionality with a modified dataset.

isPropagationStopped ( )
Returns
bool
setAccessGranted ( bool $accessGranted)
param $accessGranted

the accessGranted

getTable ( )
Returns
string
getRecord ( )
Returns
array
updateRecord ( array $record)
param $record

the record

getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context

BrokenLinkAnalysisEvent

The PSR-14 event \TYPO3\CMS\Core\Html\Event\BrokenLinkAnalysisEvent can be used to get information about broken links set in the rich text editor (RTE).

The procedure for marking the broken links in the RTE is as follow:

  1. The RTE content is fetched from the database. Before it is displayed in the edit form, RTE transformations are performed.
  2. The transformation function parses the text and detects links.
  3. For each link, a new PSR-14 event is dispatched.
  4. If a listener is attached, it may set the link as broken and will set the link as "checked".
  5. If a link is detected as broken, RTE will mark it as broken.

This functionality is implemented in the system extension linkvalidator. Other extensions can use the event to override the default behaviour.

Example

API

class BrokenLinkAnalysisEvent
Fully qualified name
\TYPO3\CMS\Core\Html\Event\BrokenLinkAnalysisEvent

Event that is fired to validate if a link is valid or not.

isPropagationStopped ( )
Returns
bool
getLinkType ( )

Returns the link type as string

Returns
string
getLinkData ( )

Returns resolved LinkService data, depending on the type

Returns
array
param $reason

the reason, default: ''

Returns
bool
getReason ( )
Returns
string

AfterMailerInitializationEvent

The PSR-14 event \TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent is fired once a new mailer is instantiated with specific transport settings. So it is possible to add custom mailing settings.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Mail\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/after-mailer-initialization'
Copied!

Read how to configure dependency injection in extensions.

An example listener, which hooks into the Mailer API to modify mailer settings to not send any emails ("null mailer"), could look like this:

EXT:my_extension/Classes/Mail/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Mail\EventListener;

use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;

final class MyEventListener
{
    public function __invoke(AfterMailerInitializationEvent $event): void
    {
        $event->getMailer()->injectMailSettings(['transport' => 'null']);
    }
}
Copied!

API

class AfterMailerInitializationEvent
Fully qualified name
\TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent

This event is fired once a new Mailer is instantiated with specific transport settings.

So it is possible to add custom mailing settings.

getMailer ( )
Returns
\Symfony\Component\Mailer\MailerInterface

AfterMailerSentMessageEvent

New in version 12.0

The PSR-14 event \TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent is dispatched as soon as the message has been sent via the corresponding \Symfony\Component\Mailer\Transport\TransportInterface. It receives the current mailer instance, which depends on the implementation - usually \TYPO3\CMS\Core\Mail\Mailer . It contains the \Symfony\Component\Mailer\SentMessage object, which can be retrieved using the getSentMessage() method.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Mail\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/process-sent-message'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Mail/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Mail\EventListener;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Mailer;

final class MyEventListener
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function __invoke(AfterMailerSentMessageEvent $event): void
    {
        $mailer = $event->getMailer();
        if (!$mailer instanceof Mailer) {
            return;
        }

        $sentMessage = $mailer->getSentMessage();
        if ($sentMessage !== null) {
            $this->logger->debug($sentMessage->getDebug());
        }
    }
}
Copied!

API

class AfterMailerSentMessageEvent
Fully qualified name
\TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent

This event is fired once a Mailer has sent a message and allows listeners to execute further code afterwards, depending on the result, e.g. the SentMessage.

Note: Usually TYPO3CMSCoreMailMailer is given to the event. This implementation allows to retrieve the SentMessage using the getSentMessage() method. Depending on the Transport, used to send the message, this might also be NULL.

getMailer ( )
Returns
\Symfony\Component\Mailer\MailerInterface

BeforeMailerSentMessageEvent

New in version 12.0

The PSR-14 event \TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent is dispatched before the message is sent by the mailer and can be used to manipulate the \Symfony\Component\Mime\RawMessage and the \Symfony\Component\Mailer\Envelope. Usually a \Symfony\Component\Mime\Email or \TYPO3\CMS\Core\Mail\FluidEmail instance is given as RawMessage. Additionally the mailer instance is given, which depends on the implementation - usually \TYPO3\CMS\Core\Mail\Mailer . It contains the \Symfony\Component\Mailer\Transport object, which can be retrieved using the getTransport() method.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Mail\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-message'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Mail/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Mail\EventListener;

use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

final class MyEventListener
{
    public function __invoke(BeforeMailerSentMessageEvent $event): void
    {
        $message = $event->getMessage();

        // If $message is an Email implementation, add an additional recipient
        if ($message instanceof Email) {
            $message->addCc(new Address('cc_recipient@example.org'));
        }
    }
}
Copied!

API

class BeforeMailerSentMessageEvent
Fully qualified name
\TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent

This event is fired before the Mailer has sent a message and allows listeners to manipulate the RawMessage and the Envelope.

Note: Usually TYPO3CMSCoreMailMailer is given to the event. This implementation allows to retrieve the TransportInterface using the getTransport() method.

getMessage ( )
Returns
\Symfony\Component\Mime\RawMessage
setMessage ( \Symfony\Component\Mime\RawMessage $message)
param $message

the message

getEnvelope ( )
Returns
?\Symfony\Component\Mailer\Envelope
setEnvelope ( ?\Symfony\Component\Mailer\Envelope $envelope = NULL)
param $envelope

the envelope, default: NULL

getMailer ( )
Returns
\Symfony\Component\Mailer\MailerInterface

AfterPackageActivationEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::afterExtensionInstall.

The PSR-14 event \TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent is triggered after a package has been activated.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Package\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/extension-activated'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Classes/Package/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Package\EventListener;

use TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent;

final class MyEventListener
{
    public function __invoke(AfterPackageActivationEvent $event)
    {
        if ($event->getPackageKey() === 'my_extension') {
            $this->executeInstall();
        }
    }

    private function executeInstall(): void
    {
        // do something
    }
}
Copied!

API

class AfterPackageActivationEvent
Fully qualified name
\TYPO3\CMS\Core\Package\Event\AfterPackageActivationEvent

Event that is triggered after a package has been activated

getPackageKey ( )
Returns
string
getType ( )
Returns
string
getEmitter ( )
Returns
?object

AfterPackageDeactivationEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::afterExtensionUninstall.

The PSR-14 event \TYPO3\CMS\Core\Package\Event\AfterPackageDeactivationEvent is triggered after a package has been deactivated.

Example

API

class AfterPackageDeactivationEvent
Fully qualified name
\TYPO3\CMS\Core\Package\Event\AfterPackageDeactivationEvent

Event that is triggered after a package has been de-activated

getPackageKey ( )
Returns
string
getType ( )
Returns
string
getEmitter ( )
Returns
?object

BeforePackageActivationEvent

The PSR-14 event \TYPO3\CMS\Core\Package\Event\BeforePackageActivationEvent is triggered before a number of packages should become active.

Example

API

class BeforePackageActivationEvent
Fully qualified name
\TYPO3\CMS\Core\Package\Event\BeforePackageActivationEvent

Event that is triggered before a number of packages should become active

getPackageKeys ( )
Returns
array

PackagesMayHaveChangedEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Core\Package\PackageManager::packagesMayHaveChanged.

The PSR-14 event \TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent is a marker event to ensure that Core is re-triggering the package ordering and package listings.

Example

API

class PackagesMayHaveChangedEvent
Fully qualified name
\TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent

Marker event to ensure that Core is re-triggering the package ordering and package listings

BeforeJavaScriptsRenderingEvent

The PSR-14 event \TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]JavaScript renders the output.

Example

API

class BeforeJavaScriptsRenderingEvent
Fully qualified name
\TYPO3\CMS\Core\Page\Event\BeforeJavaScriptsRenderingEvent

This event is fired once before TYPO3CMSCorePageAssetRenderer::render[Inline]JavaScript renders the output.

getAssetCollector ( )
Returns
\TYPO3\CMS\Core\Page\AssetCollector
isInline ( )
Returns
bool
isPriority ( )
Returns
bool

BeforeStylesheetsRenderingEvent

The PSR-14 event \TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent is fired once before \TYPO3\CMS\Core\Page\AssetRenderer::render[Inline]Stylesheets renders the output.

Example

API

class BeforeStylesheetsRenderingEvent
Fully qualified name
\TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent

This event is fired once before TYPO3CMSCorePageAssetRenderer::render[Inline]Stylesheets renders the output.

getAssetCollector ( )
Returns
\TYPO3\CMS\Core\Page\AssetCollector
isInline ( )
Returns
bool
isPriority ( )
Returns
bool

EnrichPasswordValidationContextDataEvent

New in version 12.3

The PSR-14 event \TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent allows extensions to enrich the EXT:core/Classes/PasswordPolicy/Validator/Dto/ContextData.php (GitHub) DTO used in the password policy validation.

The PSR-14 event is dispatched in all classes where a user password is validated against the globally configured password policy.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\PasswordPolicy\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/enrich-context-data-event-listener'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Redirects/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\PasswordPolicy\EventListener;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent;

final class MyEventListener
{
    public function __invoke(EnrichPasswordValidationContextDataEvent $event): void
    {
        if ($event->getInitiatingClass() === DataHandler::class) {
            $event->getContextData()->setData('currentMiddleName', $event->getUserData()['middle_name'] ?? '');
            $event->getContextData()->setData('currentEmail', $event->getUserData()['email'] ?? '');
        }
    }
}
Copied!

API

class EnrichPasswordValidationContextDataEvent
Fully qualified name
\TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent

Event is dispatched before the ContextData DTO is passed to the password policy validator.

Note, that the $userData array will include user data available from the initiating class only. Event listeners should therefore always consider the initiating class name when accessing data from getUserData().

getContextData ( )
Returns
\TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData
getUserData ( )
Returns
array
getInitiatingClass ( )
Returns
string

Resource

The following list contains PSR-14 events in EXT:core, namespace Resource.

Contents:

AfterDefaultUploadFolderWasResolvedEvent

New in version 12.3

The event can be used as an improved alternative for the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] hook, serving the same purpose.

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterDefaultUploadFolderWasResolvedEvent allows to modify the default upload folder after it has been resolved for the current page or user.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Resource\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/after-default-upload-folder-was-resolved'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Resource\Event\AfterDefaultUploadFolderWasResolvedEvent;

final class MyEventListener
{
    public function __invoke(AfterDefaultUploadFolderWasResolvedEvent $event): void
    {
        $event->setUploadFolder($event->getUploadFolder()->getStorage()->getFolder('/'));
    }
}
Copied!

API

class AfterDefaultUploadFolderWasResolvedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterDefaultUploadFolderWasResolvedEvent

Event that is fired after the default upload folder for a user was checked

getUploadFolder ( )
Returns
?\TYPO3\CMS\Core\Resource\FolderInterface
setUploadFolder ( \TYPO3\CMS\Core\Resource\FolderInterface $uploadFolder)
param $uploadFolder

the uploadFolder

getPid ( )
Returns
?int
getTable ( )
Returns
?string
getFieldName ( )
Returns
?string

AfterFileAddedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent is fired after a file was added to the resource storage / driver.

Example: Using listeners for this event allows, for example, to post-check permissions or perform specific analysis of files like additional metadata analysis after adding them to TYPO3.

Example

API

class AfterFileAddedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent

This event is fired after a file was added to the Resource Storage / Driver.

Use case: Using listeners for this event allows to e.g. post-check permissions or specific analysis of files like additional metadata analysis after adding them to TYPO3.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

AfterFileAddedToIndexEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileAddedToIndexEvent is fired once an index was just added to the database (= indexed).

Example: Using listeners for this event allows to additionally populate custom fields of the sys_file / sys_file_metadata database records.

Example

API

class AfterFileAddedToIndexEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileAddedToIndexEvent

This event is fired once an index was just added to the database (= indexed).

Examples: Allows to additionally populate custom fields of the sys_file/sys_file_metadata database records.

getFileUid ( )
Returns
int
getRecord ( )
Returns
array

AfterFileCommandProcessedEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileCommandProcessedEvent can be used to perform additional tasks for specific file commands. For example, trigger a custom indexer after a file has been uploaded.

This event is fired in the \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility class.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Resource\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/after-file-command-processed'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Resource\Event\AfterFileCommandProcessedEvent;

final class MyEventListener
{
    public function __invoke(AfterFileCommandProcessedEvent $event): void
    {
        // Do magic here
    }
}
Copied!

API

class AfterFileCommandProcessedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileCommandProcessedEvent

Event that is triggered after a file command has been processed. Can be used to perform additional tasks for specific commands. For example, trigger a custom indexer after a file has been uploaded.

getCommand ( )

A single command, e.g.

'upload' => [
    'target' => '1:/some/folder/'
    'data' => '1'
]
Copied!
Returns
array<string,array<string,mixed>>
getResult ( )
Return description

The result - Depending on the performed action,this could e.g. be a File or just a boolean.

Returns
mixed
getConflictMode ( )
Return description

The current conflict mode

Returns
string

AfterFileContentsSetEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent is fired after the contents of a file got set / replaced.

Example: Listeners can analyze content for AI purposes within extensions.

Example

API

class AfterFileContentsSetEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent

This event is fired after the contents of a file got set / replaced.

Examples: Listeners can analyze content for AI purposes within Extensions.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getContent ( )
Returns
string

AfterFileCopiedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileCopiedEvent is fired after a file was copied within a resource storage / driver. The folder represents the "target folder".

Example: Listeners can sign up for listing duplicates using this event.

Example

API

class AfterFileCopiedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileCopiedEvent

This event is fired after a file was copied within a Resource Storage / Driver.

The folder represents the "target folder".

Example: Listeners can sign up for listing duplicates using this event.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getNewFileIdentifier ( )
Returns
string
getNewFile ( )
Returns
?\TYPO3\CMS\Core\Resource\FileInterface

AfterFileCreatedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileCreatedEvent is fired after a file was created within a resource storage / driver. The folder represents the "target folder".

Example: This allows to modify a file or check for an appropriate signature after a file was created in TYPO3.

Example

API

class AfterFileCreatedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileCreatedEvent

This event is fired before a file was created within a Resource Storage / Driver.

The folder represents the "target folder".

Example: This allows to modify a file or check for an appropriate signature after a file was created in TYPO3.

getFileName ( )
Returns
string
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

AfterFileDeletedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent is fired after a file was deleted.

Example: If an extension provides additional functionality (for example variants), this event allows listeners to also clean up their custom handling. This can also be used for versioning of files.

Example

API

class AfterFileDeletedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent

This event is fired after a file was deleted.

Example: If an extension provides additional functionality (e.g. variants), this event allows listener to also clean up their custom handling. This can also be used for versioning of files.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface

AfterFileMarkedAsMissingEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileMarkedAsMissingEvent is fired once a file was just marked as missing in the database (table sys_file).

Example: If a file is marked as missing, listeners can try to recover a file. This can happen on specific setups where editors also work via FTP.

Example

API

class AfterFileMarkedAsMissingEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileMarkedAsMissingEvent

This event is fired once a file was just marked as missing in the database (sys_file).

Example: If a file is marked as missing, listeners can try to recover a file. This can happen on specific setups where editors also work via FTP.

getFileUid ( )
Returns
int

AfterFileMetaDataCreatedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataCreatedEvent is fired once metadata of a file was added to the database, so it can be enriched with more information.

Example

API

class AfterFileMetaDataCreatedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataCreatedEvent

This event is fired once metadata of a file was added to the database, so it can be enriched with more information.

getFileUid ( )
Returns
int
getMetaDataUid ( )
Returns
int
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

AfterFileMetaDataDeletedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataDeletedEvent is fired once all metadata of a file was removed, in order to manage custom metadata that was added previously.

Example

API

class AfterFileMetaDataDeletedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataDeletedEvent

This event is fired once all metadata of a file was removed, in order to manage custom metadata that was added previously

getFileUid ( )
Returns
int

AfterFileMetaDataUpdatedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataUpdatedEvent is fired once metadata of a file was updated, in order to update custom metadata fields accordingly.

Example

API

class AfterFileMetaDataUpdatedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileMetaDataUpdatedEvent

This event is fired once metadata of a file was updated, in order to update custom metadata fields accordingly

getFileUid ( )
Returns
int
getMetaDataUid ( )
Returns
int
getRecord ( )
Returns
array

AfterFileMovedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent is fired after a file was moved within a resource storage / driver. The folder represents the "target folder".

Example: Use this to update custom third-party handlers that rely on specific paths.

Example

API

class AfterFileMovedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent

This event is fired after a file was moved within a Resource Storage / Driver.

The folder represents the "target folder".

Examples: Use this to update custom third party handlers that rely on specific paths.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getOriginalFolder ( )
Returns
\TYPO3\CMS\Core\Resource\FolderInterface

AfterFileProcessingEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileProcessingEvent is fired after a file object has been processed. This allows to further customize a file object's processed file.

Example

API

class AfterFileProcessingEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileProcessingEvent

This event is fired after a file object has been processed.

This allows to further customize a file object's processed file.

getProcessedFile ( )
Returns
\TYPO3\CMS\Core\Resource\ProcessedFile
setProcessedFile ( \TYPO3\CMS\Core\Resource\ProcessedFile $processedFile)
param $processedFile

the processedFile

getDriver ( )
Returns
\TYPO3\CMS\Core\Resource\Driver\DriverInterface
getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getTaskType ( )
Returns
string
getConfiguration ( )
Returns
array

AfterFileRemovedFromIndexEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileRemovedFromIndexEvent is fired once a file was just removed in the database (table sys_file).

Example: A listener can further handle files and manage them separately outside of TYPO3's index.

Example

API

class AfterFileRemovedFromIndexEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileRemovedFromIndexEvent

This event is fired once a file was just removed in the database (sys_file).

Example can be to further handle files and manage them separately outside of TYPO3's index.

getFileUid ( )
Returns
int

AfterFileRenamedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileRenamedEvent is fired after a file was renamed in order to further process a file or filename or update custom references to a file.

Example

API

class AfterFileRenamedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileRenamedEvent

This event is fired after a file was renamed in order to further process a file or filename or update custom references to a file.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getTargetFileName ( )
Returns
?string

AfterFileReplacedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileReplacedEvent is fired after a file was replaced.

Example: Further process a file or create variants, or index the contents of a file for AI analysis etc.

Example

API

class AfterFileReplacedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileReplacedEvent

This event is fired after a file was replaced.

Example: Further process a file or create variants, or index the contents of a file for AI analysis etc.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getLocalFilePath ( )
Returns
string

AfterFileUpdatedInIndexEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFileUpdatedInIndexEvent is fired once an index was just updated inside the database (= indexed). Custom listeners can update further index values when a file was updated.

Example

API

class AfterFileUpdatedInIndexEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFileUpdatedInIndexEvent

This event is fired once an index was just updated inside the database (= indexed).

Custom listeners can update further index values when a file was updated.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\File
getRelevantProperties ( )
Returns
array
getUpdatedFields ( )
Returns
array

AfterFolderAddedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent is fired after a folder was added to the resource storage / driver.

This allows to customize permissions or set up editor permissions automatically via listeners.

Example

API

class AfterFolderAddedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent

This event is fired after a folder was added to the Resource Storage / Driver.

This allows to customize permissions or set up editor permissions automatically via listeners.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

AfterFolderCopiedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFolderCopiedEvent is fired after a folder was copied to the resource storage / driver.

Example: Custom listeners can analyze contents of a file or add custom permissions to a folder automatically.

Example

API

class AfterFolderCopiedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFolderCopiedEvent

This event is fired after a folder was copied to the Resource Storage / Driver.

Example: Custom listeners can analyze contents of a file or add custom permissions to a folder automatically.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetParentFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetFolder ( )
Returns
?\TYPO3\CMS\Core\Resource\FolderInterface

AfterFolderDeletedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFolderDeletedEvent is fired after a folder was deleted. Custom listeners can then further clean up permissions or third-party processed files with this event.

Example

API

class AfterFolderDeletedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFolderDeletedEvent

This event is fired after a folder was deleted. Custom listeners can then further clean up permissions or third-party processed files with this event.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
isDeleted ( )
Returns
bool

AfterFolderMovedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFolderMovedEvent is fired after a folder was moved within the resource storage / driver. Custom references can be updated via listeners of this event.

Example

API

class AfterFolderMovedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFolderMovedEvent

This event is fired after a folder was moved within the Resource Storage / Driver.

Custom references can be updated via listeners of this event.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetParentFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetFolder ( )
Returns
?\TYPO3\CMS\Core\Resource\FolderInterface

AfterFolderRenamedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterFolderRenamedEvent is fired after a folder was renamed.

Example: Add custom processing of folders or adjust permissions.

This event is also used by TYPO3 itself to synchronize folder relations in records (for example in the table sys_filemounts) after renaming of folders.

Example

API

class AfterFolderRenamedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterFolderRenamedEvent

This event is fired after a folder was renamed.

Examples: Add custom processing of folders or adjust permissions.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getSourceFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

AfterResourceStorageInitializationEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\AfterResourceStorageInitializationEvent is fired after a resource object was built/created. Custom handlers can be initialized at this moment for any kind of resource as well.

Example

API

class AfterResourceStorageInitializationEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\AfterResourceStorageInitializationEvent

This event is fired after a resource object was built/created.

Custom handlers can be initialized at this moment for any kind of source as well.

getStorage ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceStorage
setStorage ( \TYPO3\CMS\Core\Resource\ResourceStorage $storage)
param $storage

the storage

AfterVideoPreviewFetchedEvent

New in version 12.2

The purpose of the PSR-14 event \TYPO3\CMS\Core\Resource\OnlineMedia\Event\AfterVideoPreviewFetchedEvent is to modify the preview file of online media previews (like YouTube and Vimeo). If, for example, a processed file is bad (blank or outdated), this event can be used to modify and/or update the preview file.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Resource\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/after-video-preview-fetched'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Resource\OnlineMedia\Event\AfterVideoPreviewFetchedEvent;

final class MyEventListener
{
    public function __invoke(AfterVideoPreviewFetchedEvent $event): void
    {
        $event->setPreviewImageFilename(
            '/var/www/html/typo3temp/assets/online_media/new-preview-image.jpg',
        );
    }
}
Copied!

API

class AfterVideoPreviewFetchedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\OnlineMedia\Event\AfterVideoPreviewFetchedEvent

Allows to modify a generated YouTube/Vimeo (or other Online Media) preview images

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\File
getOnlineMediaId ( )
Returns
string
getPreviewImageFilename ( )
Returns
string
setPreviewImageFilename ( string $previewImageFilename)
param $previewImageFilename

the previewImageFilename

BeforeFileAddedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent is fired before a file is about to be added to the resource storage / driver. This allows to perform custom checks to a file or restrict access to a file before the file is added.

Example

API

class BeforeFileAddedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent

This event is fired before a file is about to be added to the Resource Storage / Driver.

This allows to do custom checks to a file or restrict access to a file before the file is added.

getFileName ( )
Returns
string
setFileName ( string $fileName)
param $fileName

the fileName

getSourceFilePath ( )
Returns
string
getTargetFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getStorage ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceStorage
getDriver ( )
Returns
\TYPO3\CMS\Core\Resource\Driver\DriverInterface

BeforeFileContentsSetEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent is fired before the contents of a file gets set / replaced. This allows to further analyze or modify the content of a file before it is written by the driver.

Example

API

class BeforeFileContentsSetEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent

This event is fired before the contents of a file gets set / replaced.

This allows to further analyze or modify the content of a file before it is written by the driver.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getContent ( )
Returns
string
setContent ( string $content)
param $content

the content

BeforeFileCopiedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileCopiedEvent is fired before a file is about to be copied within a resource storage / driver. The folder represents the "target folder".

This allows to further analyze or modify the file or metadata before it is written by the driver.

Example

API

class BeforeFileCopiedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileCopiedEvent

This event is fired before a file is about to be copied within a Resource Storage / Driver.

The folder represents the "target folder".

This allows to further analyze or modify the file or metadata before it is written by the driver.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

BeforeFileCreatedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent is fired before a file is about to be created within a resource storage / driver. The folder represents the "target folder".

This allows to further analyze or modify the file or filename before it is written by the driver.

Example

API

class BeforeFileCreatedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent

This event is fired before a file is about to be created within a Resource Storage / Driver.

The folder represents the "target folder".

This allows to further analyze or modify the file or filename before it is written by the driver.

getFileName ( )
Returns
string
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

BeforeFileDeletedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileDeletedEvent is fired before a file is about to be deleted.

Event listeners can clean up third-party references with this event.

Example

API

class BeforeFileDeletedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileDeletedEvent

This event is fired before a file is about to be deleted.

Event listeners can clean up third-party references with this event.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface

BeforeFileMovedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent is fired before a file is about to be moved within a resource storage / driver. The folder represents the "target folder".

Example

API

class BeforeFileMovedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent

This event is fired before a file is about to be moved within a Resource Storage / Driver.

The folder represents the "target folder".

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetFileName ( )
Returns
string

BeforeFileProcessingEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent is fired before a file object is processed. This allows to add further information or enrich the file before the processing is kicking in.

Example

API

class BeforeFileProcessingEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent

This event is fired before a file object is processed.

Allows to add further information or enrich the file before the processing is kicking in.

getProcessedFile ( )
Returns
\TYPO3\CMS\Core\Resource\ProcessedFile
setProcessedFile ( \TYPO3\CMS\Core\Resource\ProcessedFile $processedFile)
param $processedFile

the processedFile

getDriver ( )
Returns
\TYPO3\CMS\Core\Resource\Driver\DriverInterface
getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getTaskType ( )
Returns
string
getConfiguration ( )
Returns
array

BeforeFileRenamedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent is fired before a file is about to be renamed. Custom listeners can further rename the file according to specific guidelines based on the project.

Example

API

class BeforeFileRenamedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent

This event is fired before a file is about to be renamed. Custom listeners can further rename the file according to specific guidelines based on the project.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getTargetFileName ( )
Returns
?string

BeforeFileReplacedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent is fired before a file is about to be replaced. Custom listeners can check for file integrity or analyze the content of the file before it gets added.

Example

API

class BeforeFileReplacedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent

This event is fired before a file is about to be replaced.

Custom listeners can check for file integrity or analyze the content of the file before it gets added.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getLocalFilePath ( )
Returns
string

BeforeFolderAddedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFolderAddedEvent is fired before a folder is about to be added to the resource storage / driver. This allows to further specify folder names according to regulations for a specific project.

Example

API

class BeforeFolderAddedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFolderAddedEvent

This event is fired before a folder is about to be added to the Resource Storage / Driver.

This allows to further specify folder names according to regulations for a specific project.

getParentFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getFolderName ( )
Returns
string

BeforeFolderCopiedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFolderCopiedEvent is fired before a folder is about to be copied to the resource storage / driver. Listeners could add deferred processing / queuing of large folders.

Example

API

class BeforeFolderCopiedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFolderCopiedEvent

This event is fired before a folder is about to be copied to the Resource Storage / Driver.

Listeners could add deferred processing / queuing of large folders.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetParentFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetFolderName ( )
Returns
string

BeforeFolderDeletedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFolderDeletedEvent is fired before a folder is about to be deleted.

Listeners can use this event to clean up further external references to a folder / files in this folder.

Example

API

class BeforeFolderDeletedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFolderDeletedEvent

This event is fired before a folder is about to be deleted.

Listeners can use this event to clean up further external references to a folder / files in this folder.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder

BeforeFolderMovedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFolderMovedEvent is fired before a folder is about to be moved to the resource storage / driver. Listeners can be used to modify a folder name before it is actually moved or to ensure consistency or specific rules when moving folders.

Example

API

class BeforeFolderMovedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFolderMovedEvent

This event is fired before a folder is about to be moved to the Resource Storage / Driver.

Listeners can be used to modify a folder name before it is actually moved or to ensure consistency or specific rules when moving folders.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetParentFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetFolderName ( )
Returns
string

BeforeFolderRenamedEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeFolderRenamedEvent is fired before a folder is about to be renamed. Listeners can be used to modify a folder name before it is actually moved or to ensure consistency or specific rules when renaming folders.

Example

API

class BeforeFolderRenamedEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeFolderRenamedEvent

This event is fired before a folder is about to be renamed.

Listeners can be used to modify a folder name before it is actually moved or to ensure consistency or specific rules when renaming folders.

getFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getTargetName ( )
Returns
string

BeforeResourceStorageInitializationEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\BeforeResourceStorageInitializationEvent is fired before a resource object is actually built/created.

Example: A database record can be enriched to add dynamic values to each resource (file/folder) before the creation of a storage.

Example

API

class BeforeResourceStorageInitializationEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\BeforeResourceStorageInitializationEvent

This event is fired before a resource object is actually built/created.

Example: A database record can be enriched to add dynamic values to each resource (file/folder) before creation of a storage

getStorageUid ( )
Returns
int
setStorageUid ( int $storageUid)
param $storageUid

the storageUid

getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

getFileIdentifier ( )
Returns
?string
setFileIdentifier ( ?string $fileIdentifier)
param $fileIdentifier

the fileIdentifier

EnrichFileMetaDataEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\EnrichFileMetaDataEvent is called after a record has been loaded from database. It allows other places to perform the extension of metadata at runtime or, for example, translation and workspace overlay.

Example

API

class EnrichFileMetaDataEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\EnrichFileMetaDataEvent

Event that is called after a record has been loaded from database Allows other places to do extension of metadata at runtime or for example translation and workspace overlay.

getFileUid ( )
Returns
int
getMetaDataUid ( )
Returns
int
getRecord ( )
Returns
array
setRecord ( array $record)
param $record

the record

GeneratePublicUrlForResourceEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent is fired before TYPO3 FAL's native URL generation for a resource is instantiated.

This allows listeners to create custom links to certain files (for example restrictions) for creating authorized deep links.

Example

API

class GeneratePublicUrlForResourceEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent

This event is fired before TYPO3 FAL's native URL generation for a Resource is instantiated.

This allows for listeners to create custom links to certain files (e.g. restrictions) for creating authorized deeplinks.

getResource ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceInterface
getStorage ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceStorage
getDriver ( )
Returns
\TYPO3\CMS\Core\Resource\Driver\DriverInterface
getPublicUrl ( )
Returns
?string
setPublicUrl ( ?string $publicUrl)
param $publicUrl

the publicUrl

ModifyFileDumpEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent is fired in the \TYPO3\CMS\Core\Controller\FileDumpController and allows extensions to perform additional access / security checks before dumping a file. The event does not only contain the file to dump but also the PSR-7 Request.

In case the file dump should be rejected, the event has to set a PSR-7 response, usually with a 403 status code. This will then immediately stop the propagation.

With the event, it is not only possible to reject the file dump request, but also to replace the file, which should be dumped.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Resource\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-file-dump'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Resource/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Resource\EventListener;

use TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent;

final class MyEventListener
{
    public function __invoke(ModifyFileDumpEvent $event): void
    {
        // Do magic here
    }
}
Copied!

API

class ModifyFileDumpEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent

Event that is triggered when a file should be dumped to the browser, allowing to perform custom security/access checks when accessing a file through a direct link, and returning an alternative Response.

It is also possible to replace the file during this event, but not setting a response.

As soon as a custom Response is added, the propagation is stopped.

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceInterface
setFile ( \TYPO3\CMS\Core\Resource\ResourceInterface $file)
param $file

the file

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
setResponse ( \Psr\Http\Message\ResponseInterface $response)
param $response

the response

getResponse ( )
Returns
?\Psr\Http\Message\ResponseInterface
isPropagationStopped ( )
Returns
bool

ModifyIconForResourcePropertiesEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\ModifyIconForResourcePropertiesEvent is dispatched when an icon for a resource (file or folder) is fetched, allowing to modify the icon or overlay in an event listener.

Example

API

class ModifyIconForResourcePropertiesEvent
Fully qualified name
\TYPO3\CMS\Core\Imaging\Event\ModifyIconForResourcePropertiesEvent

This is an Event every time an icon for a resource (file or folder) is fetched, allowing to modify the icon or overlay in an event listener.

getResource ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceInterface
getSize ( )
Returns
string
getOptions ( )
Returns
array
getIconIdentifier ( )
Returns
?string
setIconIdentifier ( ?string $iconIdentifier)
param $iconIdentifier

the iconIdentifier

getOverlayIdentifier ( )
Returns
?string
setOverlayIdentifier ( ?string $overlayIdentifier)
param $overlayIdentifier

the overlayIdentifier

SanitizeFileNameEvent

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent is fired once an index was just added to the database (= indexed), so it is possible to modify the file name, and name the files according to naming conventions of a specific project.

Example

API

class SanitizeFileNameEvent
Fully qualified name
\TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent

This event is fired once an index was just added to the database (= indexed), so it is possible to modify the file name, and name the files according to naming conventions of a specific project.

getFileName ( )
Returns
string
setFileName ( string $fileName)
param $fileName

the fileName

getTargetFolder ( )
Returns
\TYPO3\CMS\Core\Resource\Folder
getStorage ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceStorage
getDriver ( )
Returns
\TYPO3\CMS\Core\Resource\Driver\DriverInterface

InvestigateMutationsEvent

New in version 12.3

The PSR-14 event \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent will be dispatched when the Content Security Policy backend module searches for potential resolutions to a specific CSP violation report. This way, third-party integrations that rely on external resources (for example, maps, file storage, content processing/translation, ...) can provide the necessary mutations.

Example

API

class InvestigateMutationsEvent
Fully qualified name
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent

Event that is dispatched when reports are handled in the CSP backend module to find potential mutations as a resolution.

public readonly policy
public readonly report
isPropagationStopped ( )
Returns
bool
stopPropagation ( )
getMutationSuggestions ( )
Returns
list<\MutationSuggestion>
setMutationSuggestions ( \TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion ...$mutationSuggestions)

Overrides all mutation suggestions (use carefully).

param $mutationSuggestions

the mutationSuggestions

appendMutationSuggestions ( \TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion ...$mutationSuggestions)
param $mutationSuggestions

the mutationSuggestions

PolicyMutatedEvent

New in version 12.3

The PSR-14 event \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent will be dispatched once all mutations have been applied to the current Content Security Policy object, just before the corresponding HTTP header is added to the HTTP response object. This allows individual adjustments for custom implementations.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\ContentSecurityPolicy\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/mutate-policy'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/ContentSecurityPolicy/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\ContentSecurityPolicy\EventListener;

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;

final class MyEventListener
{
    public function __invoke(PolicyMutatedEvent $event): void
    {
        if ($event->scope->type->isFrontend()) {
            // In our example, only the backend policy should be adjusted
            return;
        }

        // Allow images from example.org
        $event->getCurrentPolicy()->extend(
            Directive::ImgSrc,
            new UriValue('https://example.org/'),
        );
    }
}
Copied!

API

class PolicyMutatedEvent
Fully qualified name
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent
public readonly scope
public readonly defaultPolicy
isPropagationStopped ( )
Returns
bool
stopPropagation ( )
getCurrentPolicy ( )
Returns
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy
setCurrentPolicy ( \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy $currentPolicy)
param $currentPolicy

the currentPolicy

getMutationCollections ( )
Returns
list<\MutationCollection>
setMutationCollections ( \TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection ...$mutationCollections)
param $mutationCollections

the mutationCollections

ModifyTreeDataEvent

The PSR-14 event \TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent allows to modify tree data for any database tree.

Example

API

class ModifyTreeDataEvent
Fully qualified name
\TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent

Allows to modify tree data for any database tree

getTreeData ( )
Returns
\TYPO3\CMS\Backend\Tree\TreeNode
setTreeData ( \TYPO3\CMS\Backend\Tree\TreeNode $treeData)
param $treeData

the treeData

getProvider ( )
Returns
\TYPO3\CMS\Core\Tree\TableConfiguration\AbstractTableConfigurationTreeDataProvider

AfterTemplatesHaveBeenDeterminedEvent

New in version 12.0

This event is a substitution for the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing'] hook.

The PSR-14 event \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent can be used to manipulate sys_template rows. The event receives the list of resolved sys_template rows and the \Psr\Http\Message\ServerRequestInterface and allows manipulating the sys_template rows array.

The event is called in the code of the Site Management > TypoScript backend module, for example in the submodule Included TypoScript, and in the frontend.

Extensions using the old hook that want to stay compatible with TYPO3 v11 and v12 can implement both the hook and the event.

Example

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

  MyVendor\MyExtension\TypoScript\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/post-process-sys-templates'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class could look like this:

EXT:my_extension/Classes/TypoScript/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\TypoScript\EventListener;

use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent;

final class MyEventListener
{
    public function __invoke(AfterTemplatesHaveBeenDeterminedEvent $event): void
    {
        $rows = $event->getTemplateRows();

        // ... do something ...

        $event->setTemplateRows($rows);
    }
}
Copied!

API

class AfterTemplatesHaveBeenDeterminedEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent

A PSR-14 event fired when sys_template rows have been fetched.

This event is intended to add own rows based on given rows or site resolution.

getRootline ( )
Returns
array
getRequest ( )
Returns
?\Psr\Http\Message\ServerRequestInterface
getSite ( )

Convenience method to directly retrieve the Site. May be null though!

Returns
?\TYPO3\CMS\Core\Site\Entity\SiteInterface
getTemplateRows ( )
Returns
array
setTemplateRows ( array $templateRows)
param $templateRows

the templateRows

EvaluateModifierFunctionEvent

New in version 12.0

This event is a substitution of the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsparser.php']['preParseFunc'] hook.

The PSR-14 event \TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent allows custom TypoScript functions using the := operator.

Example

A simple TypoScript example looks like this:

EXT:my_extension/Configuration/TypoScript/setup.typoscript
someIdentifier = originalValue
someIdentifier := myModifierFunction(myFunctionArgument)
Copied!

To implement myModifierFunction, an extension needs to register an event listener in an extension's Services.yaml:

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

  MyVendor\MyExtension\TypoScript\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/evaluate-modifier-function'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class could look like this:

EXT:my_extension/Classes/TypoScript/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\TypoScript\EventListener;

use TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent;

final class MyEventListener
{
    public function __invoke(EvaluateModifierFunctionEvent $event): void
    {
        if ($event->getFunctionName() === 'myModifierFunction') {
            $originalValue = $event->getOriginalValue();
            $functionArgument = $event->getFunctionArgument();
            // Manipulate values and set new value
            $event->setValue($originalValue . ' example ' . $functionArgument);
        }
    }
}
Copied!

API

class EvaluateModifierFunctionEvent
Fully qualified name
\TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent

Listeners to this event are able to implement own ":=" TypoScript modifier functions, example:

foo = myOriginalValue foo := myNewFunction(myFunctionArgument)

Listeners should take care function names can not overlap with function names from other extensions and should thus namespace, example naming: "extNewsSortFunction()"

getFunctionName ( )

The function name, for example "extNewsSortFunction" when using "foo := extNewsSortFunction()"

Returns
string
getFunctionArgument ( )

Optional function argument, for example "myArgument" when using "foo := extNewsSortFunction(myArgument)"

Returns
?string
getOriginalValue ( )

Original / current value, for example "fooValue" when using: foo = fooValue foo := extNewsSortFunction(myArgument)

Returns
?string
setValue ( string $value)

Set the updated value calculated by a listener.

Note you can not set to null to "unset", since getValue() falls back to originalValue in this case. Set to empty string instead for this edge case.

param $value

the value

getValue ( )

Used by AstBuilder to fetch the updated value, falls back to given original value.

Can be used by Listeners to see if a previous listener changed the value already by comparing with getOriginalValue().

Returns
?string

BeforeFlexFormConfigurationOverrideEvent

New in version 12.3

The PSR-14 event \TYPO3\CMS\Extbase\Event\Configuration\BeforeFlexFormConfigurationOverrideEvent can be used to implement a custom FlexForm override process based on the original FlexForm configuration and the framework configuration.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Extbase\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/before-flexform-configuration-override'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Extbase/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Extbase\EventListener;

use TYPO3\CMS\Extbase\Event\Configuration\BeforeFlexFormConfigurationOverrideEvent;

final class MyEventListener
{
    public function __invoke(BeforeFlexFormConfigurationOverrideEvent $event): void
    {
        // Configuration from TypoScript
        $frameworkConfiguration = $event->getFrameworkConfiguration();

        // Configuration from FlexForm
        $originalFlexFormConfiguration = $event->getOriginalFlexFormConfiguration();

        // Currently merged configuration
        $flexFormConfiguration = $event->getFlexFormConfiguration();

        // Implement custom logic
        $flexFormConfiguration['settings']['foo'] = 'set from event listener';

        $event->setFlexFormConfiguration($flexFormConfiguration);
    }
}
Copied!

API

class BeforeFlexFormConfigurationOverrideEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Configuration\BeforeFlexFormConfigurationOverrideEvent

Event which is dispatched before flexForm configuration overrides framework configuration. Possible core flexForm overrides have already been processed in $flexFormConfiguration.

Listeners can implement a custom flexForm override process by using the original flexForm configuration available in $originalFlexFormConfiguration.

getFrameworkConfiguration ( )
Returns
array
getOriginalFlexFormConfiguration ( )
Returns
array
getFlexFormConfiguration ( )
Returns
array
setFlexFormConfiguration ( array $flexFormConfiguration)
param $flexFormConfiguration

the flexFormConfiguration

AfterRequestDispatchedEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Mvc\AfterRequestDispatchedEvent is fired after the dispatcher has successfully dispatched a request to a controller action.

Example

API

class AfterRequestDispatchedEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Mvc\AfterRequestDispatchedEvent

Event which is fired after the dispatcher has successfully dispatched a request to a controller/action.

getRequest ( )
Returns
\TYPO3\CMS\Extbase\Mvc\RequestInterface
getResponse ( )
Returns
\Psr\Http\Message\ResponseInterface

BeforeActionCallEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent is triggered before any Extbase action is called within the ActionController or one of its subclasses.

Example

API

class BeforeActionCallEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent

Event that is triggered before any Extbase Action is called within the ActionController or one of its subclasses.

getControllerClassName ( )
Returns
string
getActionMethodName ( )
Returns
string
getPreparedArguments ( )
Returns
array

AfterObjectThawedEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent allows to modify values when creating domain objects.

Example

API

class AfterObjectThawedEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent

Allows to modify values when creating domain objects.

getObject ( )
Returns
\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface
getRecord ( )
Returns
array

EntityAddedToPersistenceEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent is dispatched after persisting the object, but before updating the reference index and adding the object to the persistence session.

Example

API

class EntityAddedToPersistenceEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent

Event which is fired after an object/entity was sent to persistence layer to be added, but before updating the reference index and current session.

getObject ( )
Returns
\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityPersistedEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent is fired after an object was pushed to the storage backend.

Example

API

class EntityPersistedEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent

Event which is fired after an object was pushed to the storage backend

getObject ( )
Returns
\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityRemovedFromPersistenceEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent is fired after an object/entity was sent to the persistence layer to be removed.

Example

API

class EntityRemovedFromPersistenceEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent

Event which is fired after an object/entity was sent to persistence layer to be removed.

getObject ( )
Returns
\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

EntityUpdatedInPersistenceEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent is fired after an object/entity was persisted on update.

Example

API

class EntityUpdatedInPersistenceEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent

Event which is fired after an object/entity was sent to persistence layer to be updated.

getObject ( )
Returns
\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface

ModifyQueryBeforeFetchingObjectDataEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent is fired before the storage backend is asked for results from a given query.

Example

API

class ModifyQueryBeforeFetchingObjectDataEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent

Event which is fired before the storage backend is asked for results from a given query.

getQuery ( )
Returns
\TYPO3\CMS\Extbase\Persistence\QueryInterface
setQuery ( \TYPO3\CMS\Extbase\Persistence\QueryInterface $query)
param $query

the query

ModifyResultAfterFetchingObjectDataEvent

The PSR-14 event \TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectDataEvent is fired after the storage backend has pulled results from a given query.

Example

API

class ModifyResultAfterFetchingObjectDataEvent
Fully qualified name
\TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectDataEvent

Event which is fired after the storage backend has pulled results from a given query.

getQuery ( )
Returns
\TYPO3\CMS\Extbase\Persistence\QueryInterface
getResult ( )
Returns
array
setResult ( array $result)
param $result

the result

AfterExtensionDatabaseContentHasBeenImportedEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::afterExtensionStaticSqlImport.

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionDatabaseContentHasBeenImportedEvent is triggered after a package has imported the database file shipped within a t3d/xml import file.

Example

API

class AfterExtensionDatabaseContentHasBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionDatabaseContentHasBeenImportedEvent

Event that is triggered after a package has imported the database file shipped within a t3d/xml import file

getPackageKey ( )
Returns
string
getImportFileName ( )
Returns
string
getImportResult ( )
Returns
int
getEmitter ( )
Returns
\TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AfterExtensionFilesHaveBeenImportedEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::afterExtensionFileImport.

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionFilesHaveBeenImportedEvent is triggered after a package has imported all extension files (from Initialisation/Files/).

Example

API

class AfterExtensionFilesHaveBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionFilesHaveBeenImportedEvent

Event that is triggered after a package has imported all extension files (from Initialisation/Files)

getPackageKey ( )
Returns
string
getDestinationAbsolutePath ( )
Returns
string
getEmitter ( )
Returns
\TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AfterExtensionStaticDatabaseContentHasBeenImportedEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\Utility\InstallUtility::afterExtensionStaticSqlImport.

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent is triggered after a package has imported the database file shipped within ext_tables_static+adt.sql.

Example

API

class AfterExtensionStaticDatabaseContentHasBeenImportedEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AfterExtensionStaticDatabaseContentHasBeenImportedEvent

Event that is triggered after a package has imported the database file shipped within "ext_tables_static+adt.sql"

getPackageKey ( )
Returns
string
getSqlFileName ( )
Returns
string
getEmitter ( )
Returns
\TYPO3\CMS\Extensionmanager\Utility\InstallUtility

AvailableActionsForExtensionEvent

New in version 10.3

The event was introduced to replace the Signal/Slot \TYPO3\CMS\Extensionmanager\ViewHelper\ProcessAvailableActionsViewHelper::processActions.

The PSR-14 event \TYPO3\CMS\Extensionmanager\Event\AvailableActionsForExtensionEvent is triggered when rendering an additional action (currently within a Fluid ViewHelper) in the extension manager.

Example

API

class AvailableActionsForExtensionEvent
Fully qualified name
\TYPO3\CMS\Extensionmanager\Event\AvailableActionsForExtensionEvent

Event that is triggered when rendering an additional action (currently within a Fluid ViewHelper).

getPackageKey ( )
Returns
string
getPackageData ( )
Returns
array
getActions ( )
Returns
array
addAction ( string $actionKey, string $content)
param $actionKey

the actionKey

param $content

the content

setActions ( array $actions)
param $actions

the actions

ModifyEditFileFormDataEvent

New in version 12.1

This event can be used as an improved alternative for the removed $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/file_edit.php']['preOutputProcessingHook'] hook.

The PSR-14 event \TYPO3\CMS\Filelist\Event\ModifyEditFileFormDataEvent allows to modify the form data, used to render the file edit form in the File > Filelist module using FormEngine data compiling.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\FileList\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-edit-file-form-data'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/FileList/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FileList\EventListener;

use TYPO3\CMS\Filelist\Event\ModifyEditFileFormDataEvent;

final class MyEventListener
{
    public function __invoke(ModifyEditFileFormDataEvent $event): void
    {
        // Get current form data
        $formData = $event->getFormData();

        // Change TCA "renderType" based on the file extension
        $fileExtension = $event->getFile()->getExtension();
        if ($fileExtension === 'ts') {
            $formData['processedTca']['columns']['data']['config']['renderType'] = 'tsRenderer';
        }

        // Set updated form data
        $event->setFormData($formData);
    }
}
Copied!

API

class ModifyEditFileFormDataEvent
Fully qualified name
\TYPO3\CMS\Filelist\Event\ModifyEditFileFormDataEvent

Listeners to this event are be able to modify the form data, used to render the edit file form in the filelist module.

getFormData ( )
Returns
array
setFormData ( array $formData)
param $formData

the formData

getFile ( )
Returns
\TYPO3\CMS\Core\Resource\FileInterface
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

ProcessFileListActionsEvent

New in version 11.4

The PSR-14 event \TYPO3\CMS\Core\Configuration\Event\ProcessFileListActionsEvent is fired after generating the actions for the files and folders listing in the File > Filelist module.

This event can be used to manipulate the icons/actions, used for the edit control section in the files and folders listing within the File > Filelist module.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\FileList\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/process-file-list'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/FileList/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\FileList\EventListener;

use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

final class MyEventListener
{
    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        // do your magic
    }
}
Copied!

API

class ProcessFileListActionsEvent
Fully qualified name
\TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent

Event fired to modify icons rendered for the file listings

getResource ( )
Returns
\TYPO3\CMS\Core\Resource\ResourceInterface
isFile ( )
Returns
bool
getActionItems ( )
Returns
array
setActionItems ( array $actionItems)
param $actionItems

the actionItems

AfterCacheableContentIsGeneratedEvent

New in version 12.0

This event together with AfterCachedPageIsPersistedEvent has been introduced to serve as a direct replacement for the removed hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache']

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent can be used to decide if a page should be stored in cache.

It is executed right after all cacheable content is generated. It can also be used to manipulate the content before it is stored in TYPO3's page cache. In the Core, the event is used in EXT:indexed_search to index cacheable content.

The AfterCacheableContentIsGeneratedEvent contains the information if a generated page is able to store in cache via the $event->isCachingEnabled() method. This can be used to differentiate between the previous hooks contentPostProc-cached and contentPostProc-all. The later hook was called regardless of whether the cache was enabled or not.

Example

Registration of the AfterCacheableContentIsGeneratedEvent in your extension's Services.yaml:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/content-modifier'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

final class MyEventListener
{
    public function __invoke(AfterCacheableContentIsGeneratedEvent $event): void
    {
        // Only do this when caching is enabled
        if (!$event->isCachingEnabled()) {
            return;
        }
        $event->getController()->content = str_replace(
            'foo',
            'bar',
            $event->getController()->content,
        );
    }
}
Copied!

API

class AfterCacheableContentIsGeneratedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent

Event that allows to enhance or change content (also depending if caching is enabled).

Think of $this->isCachingEnabled() as the same as $TSFE->no_cache. Depending on disable or enabling caching, the cache is then not stored in the pageCache.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
isCachingEnabled ( )
Returns
bool
disableCaching ( )
enableCaching ( )
getCacheIdentifier ( )
Returns
string

AfterCachedPageIsPersistedEvent

New in version 12.0

This event together with AfterCacheableContentIsGeneratedEvent has been introduced to serve as a direct replacement for the removed hook:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache']

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent is commonly used to generate a static file cache. This event is only called if the page was actually stored in TYPO3's page cache.

Example

Registration of the AfterCachedPageIsPersistedEvent in your extension's Services.yaml:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/content-modifier'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent;

final class MyEventListener
{
    public function __invoke(AfterCachedPageIsPersistedEvent $event): void
    {
        // generate static file cache
    }
}
Copied!

API

class AfterCachedPageIsPersistedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent

Event that is used directly after all cached content is stored in the page cache.

If a page is called from the cache, this event is NOT fired. This event is also NOT FIRED when $TSFE->no_cache (or manipulated via AfterCacheableContentIsGeneratedEvent) is set.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
getCacheIdentifier ( )
Returns
string
getCacheData ( )
Returns
array
getCacheLifetime ( )

The amount of seconds until the cache entry is invalid.

Returns
int

AfterLinkIsGeneratedEvent

New in version 12.0

This PSR-14 event supersedes the UrlProcessorInterface logic which allowed to modify mail URNs or external URLs, but not the full anchor tag.

In addition, this PSR-14 event also replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] hook which was not executed at all times, and had a cumbersome API to modify values.

It is also recommended to use the PSR-14 event instead of the global getATagParams hook ( $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ) to add additional attributes (see example below) to links.

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent allows PHP developers to modify any kind of link generated by TYPO3's mighty typolink() functionality.

By using this event, it is possible to add attributes to links to internal pages, or links to files, as the event contains the actual information of the link type with it.

As this event works with the \TYPO3\CMS\Frontend\Typolink\LinkResultInterface object it is possible to modify or replace the LinkResult information instead of working with string replacement functionality for adding, changing or removing attributes.

If a link could not be generated, a \TYPO3\CMS\Frontend\Typolink\UnableToLinkException might be thrown.

Example

Registration of the AfterLinkIsGeneratedEvent in your extension's Services.yaml:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/link-modifier'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent;

final class MyEventListener
{
    public function __invoke(AfterLinkIsGeneratedEvent $event): void
    {
        $linkResult = $event->getLinkResult()->withAttribute(
            'data-enable-lightbox',
            'true',
        );
        $event->setLinkResult($linkResult);
    }
}
Copied!

API

class AfterLinkIsGeneratedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent

Generic event to modify any kind of link generation with typolink(). This is processed by all frontend-related links.

If a link could not be generated, a "UnableToLinkException" could be thrown by an Event Listener.

setLinkResult ( \TYPO3\CMS\Frontend\Typolink\LinkResultInterface $linkResult)

Update a link when a part was modified by an Event Listener.

param $linkResult

the linkResult

getLinkResult ( )
Returns
\TYPO3\CMS\Frontend\Typolink\LinkResultInterface
getContentObjectRenderer ( )
Returns
\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
getLinkInstructions ( )

Returns the original instructions / $linkConfiguration that were used to build the link

Returns
array

AfterPageAndLanguageIsResolvedEvent

New in version 12.0

This PSR-14 event replaces the following hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess']

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent is fired in the frontend process after a given page has been resolved including its language.

This event modifies TYPO3's language resolution logic through custom additions. It also allows sending a custom response via event listeners (for example, a custom 403 response).

Example

API

class AfterPageAndLanguageIsResolvedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent

A PSR-14 event fired in the frontend process after a given page has been resolved including its language.

This event is intended to e.g. modify TYPO3's language resolving logic by custom additions. This event also allows to send a custom Response via Event Listeners (e.g. a custom 403 response)

getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getResponse ( )
Returns
?\Psr\Http\Message\ResponseInterface
setResponse ( ?\Psr\Http\Message\ResponseInterface $response)
param $response

the response

AfterPageWithRootLineIsResolvedEvent

New in version 12.0

This PSR-14 event replaces the following hooks:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing']
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess']

The PSR-14 event \TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent fires in the frontend process after a given page has been resolved with permissions, root line, etc.

This is useful for modifying the page and root (but before resolving the language), to direct or load content from another page, or for modifying the page response if additional permissions should be checked.

Example

API

class AfterPageWithRootLineIsResolvedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent

A PSR-14 event fired in the frontend process after a given page has been resolved with permissions, rootline etc.

This is useful to modify the page + rootline (but before the language is resolved) to direct or load content from a different page, or modify the page response if additional permissions should be checked.

getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
setResponse ( \Psr\Http\Message\ResponseInterface $response)
param $response

the response

getResponse ( )
Returns
?\Psr\Http\Message\ResponseInterface

BeforePageIsResolvedEvent

New in version 12.0

This PSR-14 event replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] hook.

The PSR-14 event \TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent is processed when the main class TypoScriptFrontendController is resolving a page and its root line, based on the incoming request.

The event fires before the frontend process attempts to fully resolve a given page based on its page ID and request. Event listeners can modify incoming parameters (such as $controller->id) or the context for resolving a page.

Example

API

class BeforePageIsResolvedEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent

A PSR-14 event fired before the frontend process is trying to fully resolve a given page by its page ID and the request.

Event Listeners can modify incoming parameters (such as $controller->id) or modify the context for resolving a page.

getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

FilterMenuItemsEvent

New in version 12.0

This event has been introduced to serve as a more powerful and flexible alternative for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/tslib/class.tslib_menu.php']['filterMenuPages'] .

The PSR-14 event \TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent has a variety of properties and getters, along with TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent::getFilteredMenuItems() and TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent::setFilteredMenuItems(). Those methods can be used to change the items of a menu, which has been generated with a TypoScript HMENU or a MenuProcessor.

This event is fired after TYPO3 has filtered all menu items. The menu can then be adjusted by adding, removing or modifying the menu items. Also changing the order is possible.

Additionally, more information about the currently rendered menu, such as the menu items which were filtered out, is available.

Example

API

class FilterMenuItemsEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent

Listeners to this Event will be able to modify items for a menu generated with HMENU

getAllMenuItems ( )
Returns
array
getFilteredMenuItems ( )
Returns
array
setFilteredMenuItems ( array $filteredMenuItems)
param $filteredMenuItems

the filteredMenuItems

getMenuConfiguration ( )
Returns
array
getItemConfiguration ( )
Returns
array
getBannedMenuItems ( )
Returns
array
getExcludedDoktypes ( )
Returns
array
getSite ( )
Returns
\TYPO3\CMS\Core\Site\Entity\Site
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context
getCurrentPage ( )
Returns
array

ModifyCacheLifetimeForPageEvent

New in version 12.0

This event serves as a successor for the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] hook.

This event allows to modify the lifetime of how long a rendered page of a frontend call should be stored in the "pages" cache.

Example

Register the listener:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/cache-timeout'
Copied!

Read how to configure dependency injection in extensions.

The following listener limits the cache lifetime to 30 seconds in development context:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;

final class MyEventListener
{
    public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
    {
        // Only cache all pages for 30 seconds when in development context
        if (Environment::getContext()->isDevelopment()) {
            $event->setCacheLifetime(30);
        }
    }
}
Copied!

API

class ModifyCacheLifetimeForPageEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent

Event to allow listeners to modify the amount of seconds that a generated frontend page should be cached in the "pages" cache when initially generated.

setCacheLifetime ( int $cacheLifetime)
param $cacheLifetime

the cacheLifetime

getCacheLifetime ( )
Returns
int
getPageId ( )
Returns
int
getPageRecord ( )
Returns
array
getRenderingInstructions ( )
Returns
array
getContext ( )
Returns
\TYPO3\CMS\Core\Context\Context

ModifyHrefLangTagsEvent

The PSR-14 event \TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent is available to alter the hreflang tags just before they get rendered.

The class \TYPO3\CMS\Seo\HrefLang\HrefLangGenerator (identifier typo3-seo/hreflangGenerator) is also available as an event. Its purpose is to provide the default hreflang tags. This way it is possible to register a custom event listener after or instead of this implementation.

Example

An example implementation could look like this:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/cache-timeout'
        after: 'typo3-seo/hreflangGenerator'
Copied!

Read how to configure dependency injection in extensions.

With after and before, you can make sure your own listener is executed after or before the given identifiers.

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

final class MyEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        $hrefLangs = $event->getHrefLangs();
        $request = $event->getRequest();

        // Do anything you want with $hrefLangs
        $hrefLangs = [
            'en-US' => 'https://example.org',
            'nl-NL' => 'https://example.org/nl',
        ];

        // Override all hrefLang tags
        $event->setHrefLangs($hrefLangs);

        // Or add a single hrefLang tag
        $event->addHrefLang('de-DE', 'https://example.org/de');
    }
}
Copied!

API

class ModifyHrefLangTagsEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent

Listeners to this event will be able to modify the hreflang tags that will be generated. You can use this when you have an edge case language scenario and need to alter the default hreflang tags.

getHrefLangs ( )
Returns
array
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
setHrefLangs ( array $hrefLangs)

Set the hreflangs. This should be an array in format:

[
    'en-US' => 'https://example.com',
    'nl-NL' => 'https://example.com/nl'
]
Copied!
param $hrefLangs

the hrefLangs

addHrefLang ( string $languageCode, string $url)

Add a hreflang tag to the current list of hreflang tags

param $languageCode

The language of the hreflang tag you would like to add. For example: nl-NL

param $url

The URL of the translation. For example: https://example.com/nl

ModifyPageLinkConfigurationEvent

New in version 12.0

This event has been introduced to serve as a more powerful and flexible alternative for the removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] .

The PSR-14 event \TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent is called after a page has been resolved, and includes arguments such as the generated fragment and the to-be-used query parameters.

The page to be linked to can also be modified to link to a different page.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-page-link-configuration'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent;

final class MyEventListener
{
    public function __invoke(ModifyPageLinkConfigurationEvent $event): void
    {
        // Do your magic here
    }
}
Copied!

API

class ModifyPageLinkConfigurationEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent

A generic PSR 14 Event to allow modifying the incoming (and resolved) page when building a "page link".

This event allows Event Listener to change the page to be linked to, or add/remove possible query parameters / fragments to be generated.

getConfiguration ( )
Returns
array
setConfiguration ( array $configuration)
param $configuration

the configuration

getLinkDetails ( )
Returns
array
getPage ( )
Returns
array
setPage ( array $page)
param $page

the page

getQueryParameters ( )
Returns
array
setQueryParameters ( array $queryParameters)
param $queryParameters

the queryParameters

getFragment ( )
Returns
string
setFragment ( string $fragment)
param $fragment

the fragment

pageWasModified ( )
Returns
bool

ModifyResolvedFrontendGroupsEvent

New in version 11.5

This event is intended to restore the functionality found in the getGroupsFE authentication service that was removed in TYPO3 v11.

The PSR-14 event \TYPO3\CMS\Frontend\Authentication\ModifyResolvedFrontendGroupsEvent event allows frontend groups to be added to a (frontend) request, regardless of whether a user is logged in or not.

Example

API

class ModifyResolvedFrontendGroupsEvent
Fully qualified name
\TYPO3\CMS\Frontend\Authentication\ModifyResolvedFrontendGroupsEvent

Event listener to allow to add custom Frontend Groups to a (frontend) request regardless if a user is logged in or not.

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getUser ( )
Returns
\TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication
getGroups ( )
Returns
array
setGroups ( array $groups)
param $groups

the groups

ShouldUseCachedPageDataIfAvailableEvent

New in version 12.0

The PSR-14 event \TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent allows TYPO3 extensions to register event listeners to modify if a page should be read from cache (if it has been created in store already), or if it should be re-built completely ignoring the cache entry for the request.

This event can be used to avoid loading from the cache when indexing via CLI happens from an external source, or if the cache should be ignored when logged in from a certain IP address.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Frontend\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/avoid-cache-loading'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Frontend/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Frontend\EventListener;

use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;

final class MyEventListener
{
    public function __invoke(ShouldUseCachedPageDataIfAvailableEvent $event): void
    {
        if (!($event->getRequest()->getServerParams()['X-SolR-API'] ?? null)) {
            return;
        }
        $event->setShouldUseCachedPageData(false);
    }
}
Copied!

API

class ShouldUseCachedPageDataIfAvailableEvent
Fully qualified name
\TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent

Event to allow listeners to disable the loading of cached page data when a page is requested.

Does not have any effect if "no_cache" is activated, or if there is no cached version of a page.

getController ( )
Returns
\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
shouldUseCachedPageData ( )
Returns
bool
setShouldUseCachedPageData ( bool $shouldUseCachedPageData)
param $shouldUseCachedPageData

the shouldUseCachedPageData

BeforeRedirectEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\BeforeRedirectEvent is triggered before a redirect is made.

New in version 11.5.26/12.3

The methods setRedirectUrl() and getRequest() are available.

Example

API

class BeforeRedirectEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\BeforeRedirectEvent

Notification before a redirect is made, which also allows to modify the actual redirect URL. Setting the redirect to an empty string will avoid triggering a redirect.

getLoginType ( )
Returns
string
getRedirectUrl ( )
Returns
string
setRedirectUrl ( string $redirectUrl)
param $redirectUrl

the redirectUrl

getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface

LoginConfirmedEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\LoginConfirmedEvent is triggered when a login was successful.

Example

API

class LoginConfirmedEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\LoginConfirmedEvent

A notification when a log in has successfully arrived at the plugin, via the view and the controller, multiple information can be overridden in Event Listeners.

getController ( )
Returns
\TYPO3\CMS\FrontendLogin\Controller\LoginController
getView ( )
Returns
\TYPO3Fluid\Fluid\View\ViewInterface

LoginErrorOccurredEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\LoginErrorOccurredEvent is triggered when an error occurs while trying to log in a user.

Example

API

class LoginErrorOccurredEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\LoginErrorOccurredEvent

A notification if something went wrong while trying to log in a user.

LogoutConfirmedEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent is triggered when a logout was successful.

Example: Delete stored private key from disk on logout

Upon logout a private key the user uploaded for decryption of private information should be deleted at once. There is only a logout event if the user actively clicks the logout button, so if the user would just close the browser window there would be no LogoutConfirmedEvent. For this case we need a second line of defense like a scheduler task (out of scope of this example).

The currently logged-in user derived from the \TYPO3\CMS\Core\Context\Context is now an anonymous user that is not logged in. The information on which user just logged out cannot be determined from the context or the methods from the event. We therefore need different logic to determine the user who just logged out. This logic is not part of the example below.

Register the event listener in the Services.yaml:

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

  MyVendor\MyExtension\EventListener\DeletePrivateKeyOnLogout:
    tags:
      - name: event.listener
        identifier: 'my-extension/delete-private-key-on-logout'
Copied!

Read how to configure dependency injection in extensions.

EXT:my_extension/Classes/EventListeners/DeletePrivateKeyOnLogout.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use MyVendor\MyExtension\KeyPairHandling\KeyFileService;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent;

final class LogoutEventListener
{
    public function __construct(
        private readonly KeyFileService $keyFileService,
        private readonly Context $context,
    ) {}

    public function __invoke(LogoutConfirmedEvent $event): void
    {
        $userAspect = $this->context->getAspect('frontend.user');
        assert($userAspect instanceof UserAspect);
        if ($this->keyFileService->deletePrivateKey($userAspect)) {
            $event->getController()->addFlashMessage('Your private key has been deleted. ', '', ContextualFeedbackSeverity::NOTICE);
        } else {
            $event->getController()->addFlashMessage('Deletion of your private key failed. It will be deleted automatically within 15 minutes by a scheduler task. ', '', ContextualFeedbackSeverity::WARNING);
        }
    }
}
Copied!

API

class LogoutConfirmedEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent

A notification when a log out has successfully arrived at the plugin, via the view and the controller, multiple information can be overridden in Event Listeners.

getController ( )
Returns
\TYPO3\CMS\FrontendLogin\Controller\LoginController
getView ( )
Returns
\TYPO3Fluid\Fluid\View\ViewInterface

ModifyLoginFormViewEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent allows to inject custom variables into the login form.

Changed in version 12.0

The interface \TYPO3\CMS\Extbase\Mvc\View\ViewInterface has been removed with v12. The getView() method signature has been changed to \TYPO3Fluid\Fluid\View\ViewInterface with the v12 release.

Example

API

class ModifyLoginFormViewEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent

Allows to inject custom variables into the login form.

getView ( )
Returns
\TYPO3Fluid\Fluid\View\ViewInterface

PasswordChangeEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent contains information about the password that has been set and will be stored in the database shortly.

Deprecated since version 12.4

The PasswordChangeEvent should not be used to validate user passwords, since it is not possible to visualize to the user why a password has been rejected. Therefore the following methods of the event are deprecated:

  • ->setAsInvalid()
  • ->getErrorMessage()
  • ->isPropagationStopped()
  • ->setHashedPassword()

A custom password policy validator should be used to validate user passwords.

Example

API

class PasswordChangeEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent

Informal event that contains information about the password which was set, and is about to be stored in the database.

getUser ( )
Returns
array
getHashedPassword ( )
Returns
string
setHashedPassword ( string $passwordHash)

Deprecated: will be removed in TYPO3 13

param $passwordHash

the passwordHash

getRawPassword ( )
Returns
string
setAsInvalid ( string $message)

Deprecated: will be removed in TYPO3 13

param $message

the message

getErrorMessage ( )

Deprecated: will be removed in TYPO3 13

Returns
?string
isPropagationStopped ( )

Deprecated: will be removed in TYPO3 13

Returns
bool

SendRecoveryEmailEvent

The PSR-14 event \TYPO3\CMS\FrontendLogin\Event\SendRecoveryEmailEvent contains the email to be sent and additional information about the user who requested a new password.

Example

API

class SendRecoveryEmailEvent
Fully qualified name
\TYPO3\CMS\FrontendLogin\Event\SendRecoveryEmailEvent

Event that contains the email to be sent to the user when they request a new password.

More

Additional validation can happen here.

getUserInformation ( )
Returns
array
getEmail ( )
Returns
\TYPO3\CMS\Core\Mail\FluidEmail

BeforeImportEvent

The PSR-14 event \TYPO3\CMS\Impexp\Event\BeforeImportEvent is triggered when an import file is about to be imported.

Example

API

New in version 12.4.10

The method getFile() has been added.

class BeforeImportEvent
Fully qualified name
\TYPO3\CMS\Impexp\Event\BeforeImportEvent

This event is triggered when an import file is about to be imported

getImport ( )
Returns
\TYPO3\CMS\Impexp\Import
getFile ( )

The file being about to be imported

Returns
string

ModifyInfoModuleContentEvent

New in version 12.0

This event has been introduced as a more powerful and flexible alternative to $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/web_info/class.tx_cms_webinfo.php']['drawFooterHook'] which has now been removed.

The PSR-14 event \TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent allows the content above and below the info module to be modified. The content added in the event is displayed in each submodule of Web > Info.

The event also provides the getCurrentModule() method, which returns the current requested submodule. It is therefore possible to limit the added content to a subset of the available submodules.

Next to getRequest() and the getModuleTemplate() methods this event also features getters and setters for the header and footer content.

Access control

The added content is by default always displayed. This event provides the hasAccess() method, returning whether the access checks in the module were passed by the user.

This way, event listeners can decide on their own, whether their content should always be shown, or only if a user also has access to the main module content.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\Info\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/content-to-info-module'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/Info/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Info\EventListener;

use TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent;

final class MyEventListener
{
    public function __invoke(ModifyInfoModuleContentEvent $event): void
    {
        // Add header content for the "Localization overview" submodule,
        // if user has access to module content
        if (
            $event->hasAccess() &&
            $event->getCurrentModule()->getIdentifier() === 'web_info_translations'
        ) {
            $event->addHeaderContent('<h3>Additional header content</h3>');
        }
    }
}
Copied!

API

class ModifyInfoModuleContentEvent
Fully qualified name
\TYPO3\CMS\Info\Controller\Event\ModifyInfoModuleContentEvent

Listeners to this Event will be able to modify the header and footer content of the info module

hasAccess ( )

Whether the current user has access to the main content of the info module.

IMPORTANT: This is only for informational purposes. Listeners can therefore decide on their own if their content should be added to the module even if the user does not have access to the main module content.

Returns
bool
getRequest ( )
Returns
\Psr\Http\Message\ServerRequestInterface
getCurrentModule ( )
Returns
\TYPO3\CMS\Backend\Module\ModuleInterface
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

ModifyLanguagePackRemoteBaseUrlEvent

The PSR-14 event \TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent allows to modify the main URL of a language pack.

Example

Registration of the event listener in the extension's Services.yaml:

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

  MyVendor\MyExtension\EventListener\CustomMirror:
    tags:
      - name: event.listener
        identifier: 'my-extension/custom-mirror'
Copied!

Read how to configure dependency injection in extensions.

The corresponding event listener class:

EXT:my_extension/Classes/EventListener/CustomMirror.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;

final class CustomMirror
{
    private const EXTENSION_KEY = 'my_extension';
    private const MIRROR_URL = 'https://example.org/typo3-packages/';

    public function __invoke(ModifyLanguagePackRemoteBaseUrlEvent $event): void
    {
        if ($event->getPackageKey() === self::EXTENSION_KEY) {
            $event->setBaseUrl(new Uri(self::MIRROR_URL));
        }
    }
}
Copied!

API

class ModifyLanguagePackRemoteBaseUrlEvent
Fully qualified name
\TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent

Event to modify the main URL of a language

getBaseUrl ( )
Returns
\Psr\Http\Message\UriInterface
setBaseUrl ( \Psr\Http\Message\UriInterface $baseUrl)
param $baseUrl

the baseUrl

getPackageKey ( )
Returns
string

ModifyLanguagePacksEvent

New in version 12.2

The PSR-14 event \TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent allows to ignore extensions or individual language packs for extensions when downloading language packs.

The options of the language:update command can be used to further restrict the download (ignore additional extensions or download only certain languages), but not to ignore decisions made by the event.

Example

Registration of the event:

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

  MyVendor\MyExtension\Install\EventListener\MyEventListener:
    tags:
      - name: event.listener
        identifier: 'my-extension/modify-language-packs'
Copied!

Read how to configure dependency injection in extensions.

An implementation of the event listener:

EXT:my_extension/Classes/Install/EventListener/MyEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Install\EventListener;

use TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent;

final class MyEventListener
{
    public function __invoke(ModifyLanguagePacksEvent $event): void
    {
        $extensions = $event->getExtensions();
        foreach ($extensions as $key => $extension) {
            // Do not download language packs from Core extensions
            if ($extension['type'] === 'typo3-cms-framework') {
                $event->removeExtension($key);
            }
        }

        // Remove German language pack from EXT:styleguide
        $event->removeIsoFromExtension('de', 'styleguide');
    }
}
Copied!

API

class ModifyLanguagePacksEvent
Fully qualified name
\TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent

Event to modify the language pack array

getExtensions ( )
Returns
array
removeExtension ( string $extension)
param $extension

the extension

removeIsoFromExtension ( string $iso, string $extension)
param $iso

the iso

param $extension

the extension

BeforeRecordIsAnalyzedEvent

The PSR-14 event \TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent allows to modify results (= add results) or modify the record before LinkValidator analyzes the record.

Example

In this example we are checking if there are external links containing the URL of the project itself, as editors tend to set external links on internal pages at times.

The following code can be put in a custom extension, for example kickstarted with make. You can find a live example in our example extension EXT:examples.

Create a class that works as event listener. This class does not implement or extend any class. It has to provide a method that accepts an event of type \TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent . By default, the method is called __invoke:

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
use TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent;

final class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    public function __invoke(BeforeRecordIsAnalyzedEvent $event): void
    {
        $table = $event->getTableName();
        if ($table !== self::TABLE_NAME) {
            return;
        }
        $results = $event->getResults();
        $record = $event->getRecord();
        $field = (string)$record[self::FIELD_NAME];
        if (!str_contains($field, self::LOCAL_DOMAIN)) {
            return;
        }
        $results = $this->parseField($record, $results);
        $event->setResults($results);
    }
}
Copied!

The listener must then be registered in the extensions Services.yaml:

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

  T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener:
    tags:
      - name: event.listener
        identifier: 'txExampleCheckExternalLinksToLocalPages'
Copied!

Read how to configure dependency injection in extensions.

For the implementation we need the \TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository to register additional link errors and the \TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory so we can automatically parse for links. These two classes have to be injected via dependency injection:

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory;
use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;

final class CheckExternalLinksToLocalPagesEventListener
{

    public function __construct(
        private readonly BrokenLinkRepository $brokenLinkRepository,
        private readonly SoftReferenceParserFactory $softReferenceParserFactory,
    ) {}
}
Copied!

Now we use the SoftReferenceParserFactory to find all registered link parsers for a soft reference. Then we apply each of these parsers in turn to the configured field in the current record. For each link found we can now match, if it is an external link to an internal page.

Class T3docs\Examples\EventListener\LinkValidator\CheckExternalLinksToLocalPagesEventListener
final class CheckExternalLinksToLocalPagesEventListener
{
    private const LOCAL_DOMAIN = 'example.org';
    private const TABLE_NAME = 'tt_content';
    private const FIELD_NAME = 'bodytext';

    private function parseField(array $record, array $results): array
    {
        $conf = $GLOBALS['TCA'][self::TABLE_NAME]['columns'][self::FIELD_NAME]['config'];
        foreach ($this->findAllParsers($conf) as $softReferenceParser) {
            $parserResult = $softReferenceParser->parse(
                self::TABLE_NAME,
                self::FIELD_NAME,
                $record['uid'],
                (string)$record[self::FIELD_NAME]
            );
            if (!$parserResult->hasMatched()) {
                continue;
            }
            foreach ($parserResult->getMatchedElements() as $matchedElement) {
                if (!isset($matchedElement['subst'])) {
                    continue;
                }
                $this->matchUrl(
                    (string)$matchedElement['subst']['tokenValue'],
                    $record,
                    $results
                );
            }
        }
        return $results;
    }

    private function findAllParsers(array $conf): iterable
    {
        return $this->softReferenceParserFactory->getParsersBySoftRefParserList(
            $conf['softref'],
            ['subst']
        );
    }
}
Copied!

If the URL found in the matching is external and contains the local domain name we add an entry to the